Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99eca8b2e5 | ||
|
|
55e3b5d9e0 |
+1
-1
@@ -29,7 +29,7 @@
|
||||
stroke-dasharray: 90, 150;
|
||||
stroke-dashoffset: 0;
|
||||
stroke-width: 2;
|
||||
stroke: #06b6d4;
|
||||
stroke: #4073fa;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
/**
|
||||
* 将标准双 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)' : ''}`)
|
||||
@@ -523,6 +523,18 @@ export function prescriptionOrderLogs(params: { id: number }) {
|
||||
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 }) {
|
||||
return request.post({ url: '/tcm.prescriptionOrder/updateAmount', params })
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
<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>
|
||||
@@ -1,60 +0,0 @@
|
||||
<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>
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
<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>
|
||||
@@ -1,5 +0,0 @@
|
||||
<template>
|
||||
<div class="admin-page-actions">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,16 +0,0 @@
|
||||
<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>
|
||||
@@ -1,27 +0,0 @@
|
||||
<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,18 +17,14 @@ defineProps({
|
||||
|
||||
<style scoped lang="scss">
|
||||
.footer-btns {
|
||||
height: 64px;
|
||||
|
||||
height: 60px;
|
||||
&__content {
|
||||
bottom: 0;
|
||||
height: 64px;
|
||||
height: 60px;
|
||||
right: 0;
|
||||
left: 0;
|
||||
z-index: 99;
|
||||
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;
|
||||
@apply flex justify-center items-center shadow bg-body;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,228 +0,0 @@
|
||||
<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 = {
|
||||
showCrumb: false,
|
||||
showLogo: true,
|
||||
isUniqueOpened: true,
|
||||
sideWidth: 248,
|
||||
sideTheme: 'dark',
|
||||
sideDarkColor: '#060d18',
|
||||
openMultipleTabs: true,
|
||||
theme: '#06b6d4',
|
||||
successTheme: '#10b981',
|
||||
warningTheme: '#f59e0b',
|
||||
dangerTheme: '#ef4444',
|
||||
errorTheme: '#ef4444',
|
||||
infoTheme: '#6366f1'
|
||||
showCrumb: true, // 是否显示面包屑
|
||||
showLogo: false, // 是否显示logo
|
||||
isUniqueOpened: true, //只展开一个一级菜单
|
||||
sideWidth: 183, //侧边栏宽度
|
||||
sideTheme: 'dark', //侧边栏主题
|
||||
sideDarkColor: '#1d2124', //侧边栏深色主题颜色
|
||||
openMultipleTabs: true, // 是否开启多标签tab栏
|
||||
theme: '#4A5DFF', //主题色
|
||||
successTheme: '#67c23a', //成功主题色
|
||||
warningTheme: '#e6a23c', //警告主题色
|
||||
dangerTheme: '#f56c6c', //危险主题色
|
||||
errorTheme: '#f56c6c', //错误主题色
|
||||
infoTheme: '#909399' //信息主题色
|
||||
}
|
||||
|
||||
export const SETTING_SCHEMA_VERSION = 6
|
||||
/** 本地 setting 缓存结构版本。提升后仅对低于该版本的老缓存执行 SETTING_SCHEMA_MIGRATIONS */
|
||||
export const SETTING_SCHEMA_VERSION = 1
|
||||
|
||||
/**
|
||||
* 按版本写入 defaultSetting 中的键(老用户 localStorage 会长期盖住 config 默认值)。
|
||||
* 以后若要再推一批新默认值:把 SETTING_SCHEMA_VERSION +1,并为本版本追加一条迁移键列表。
|
||||
*/
|
||||
export const SETTING_SCHEMA_MIGRATIONS: Record<number, (keyof typeof defaultSetting)[]> = {
|
||||
1: ['sideTheme', 'sideDarkColor'],
|
||||
2: ['theme', 'successTheme', 'warningTheme', 'dangerTheme', 'errorTheme', 'infoTheme', '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']
|
||||
1: ['sideTheme', 'sideDarkColor']
|
||||
}
|
||||
|
||||
export default defaultSetting
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<el-breadcrumb class="app-breadcrumb" separator="/">
|
||||
<el-breadcrumb class="app-breadcrumb">
|
||||
<el-breadcrumb-item v-for="item in breadcrumbs" :key="item.path">
|
||||
{{ item.meta.title }}
|
||||
</el-breadcrumb-item>
|
||||
@@ -21,24 +21,22 @@ useWatchRoute((route) => {
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped lang="scss">
|
||||
.app-breadcrumb {
|
||||
:deep(.el-breadcrumb__item) {
|
||||
.el-breadcrumb__inner {
|
||||
color: var(--el-text-color-secondary);
|
||||
color: #303133;
|
||||
font-weight: 500;
|
||||
font-size: var(--el-font-size-small);
|
||||
}
|
||||
|
||||
|
||||
&:last-child .el-breadcrumb__inner {
|
||||
color: var(--el-text-color-primary);
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
:deep(.el-breadcrumb__separator) {
|
||||
color: var(--el-text-color-placeholder);
|
||||
font-weight: 400;
|
||||
color: #606266;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<header class="header">
|
||||
<div class="navbar">
|
||||
<div class="flex-1 flex items-center gap-1 min-w-0">
|
||||
<div class="flex-1 flex">
|
||||
<div class="navbar-item">
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
@@ -17,14 +17,11 @@
|
||||
<refresh />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div
|
||||
class="hidden md:flex items-center min-w-0 px-2"
|
||||
v-if="settingStore.showCrumb && breadcrumbs.length"
|
||||
>
|
||||
<div class="flex items-center px-2" v-if="!isMobile && settingStore.showCrumb">
|
||||
<breadcrumb />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="flex">
|
||||
<div class="navbar-item" v-if="!isMobile">
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
@@ -39,7 +36,12 @@
|
||||
<user-drop-down />
|
||||
</div>
|
||||
<div class="navbar-item">
|
||||
<el-tooltip class="box-item" effect="dark" content="主题设置" placement="bottom">
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="dark"
|
||||
content="主题设置"
|
||||
placement="bottom"
|
||||
>
|
||||
<setting />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
@@ -50,10 +52,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { RouteLocationMatched } from 'vue-router'
|
||||
import { useFullscreen } from '@vueuse/core'
|
||||
|
||||
import { useWatchRoute } from '@/hooks/useWatchRoute'
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
import useSettingStore from '@/stores/modules/setting'
|
||||
|
||||
@@ -70,20 +70,14 @@ const isMobile = computed(() => appStore.isMobile)
|
||||
const isCollapsed = computed(() => appStore.isCollapsed)
|
||||
const settingStore = useSettingStore()
|
||||
const { isFullscreen } = useFullscreen()
|
||||
|
||||
const breadcrumbs = ref<RouteLocationMatched[]>([])
|
||||
useWatchRoute((route) => {
|
||||
breadcrumbs.value = route.matched.filter((item) => item.meta && item.meta.title)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.navbar {
|
||||
height: var(--navbar-height);
|
||||
@apply flex px-3 bg-body;
|
||||
|
||||
@apply flex px-2 bg-body;
|
||||
.navbar-item {
|
||||
@apply h-full flex justify-center items-center;
|
||||
@apply h-full flex justify-center items-center hover:bg-page;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="app-tabs flex bg-body">
|
||||
<div class="flex-1 min-w-0 pl-3">
|
||||
<div class="app-tabs pl-4 flex bg-body">
|
||||
<div class="flex-1 min-w-0">
|
||||
<el-tabs
|
||||
:model-value="currentTab"
|
||||
:closable="tabsLists.length > 1"
|
||||
@@ -13,7 +13,7 @@
|
||||
</el-tabs>
|
||||
</div>
|
||||
<el-dropdown @command="handleCommand">
|
||||
<span class="tabs-more-btn flex items-center px-3">
|
||||
<span class="flex items-center px-3">
|
||||
<icon :size="16" name="el-icon-arrow-down" />
|
||||
</span>
|
||||
<template #dropdown>
|
||||
@@ -60,84 +60,61 @@ const handleCommand = (command: any) => {
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.app-tabs {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@apply border-t border-br;
|
||||
:deep(.el-tabs) {
|
||||
height: var(--tabs-height);
|
||||
|
||||
height: 40px;
|
||||
.el-tabs {
|
||||
&__header {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&__nav-next,
|
||||
&__nav-prev {
|
||||
@apply text-lg;
|
||||
@apply text-xl;
|
||||
}
|
||||
|
||||
&__nav-wrap::after {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
&__item {
|
||||
font-weight: 600;
|
||||
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;
|
||||
font-weight: normal;
|
||||
padding: 0 15px !important;
|
||||
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 {
|
||||
color: var(--admin-brand-primary-dark);
|
||||
background: var(--el-bg-color);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-bottom-color: transparent;
|
||||
box-shadow: var(--el-box-shadow-lighter);
|
||||
|
||||
&::before,
|
||||
color: var(--el-text-color-primary);
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
&::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background-color: var(--el-color-primary);
|
||||
margin-right: 6px;
|
||||
border-radius: 50%;
|
||||
vertical-align: 2px;
|
||||
}
|
||||
&::after {
|
||||
display: none;
|
||||
position: absolute;
|
||||
content: '';
|
||||
display: block;
|
||||
top: 0;
|
||||
height: 2px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
background-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.is-icon-close {
|
||||
color: var(--el-text-color-placeholder);
|
||||
color: var(--el-text-color-regular);
|
||||
vertical-align: -2px;
|
||||
border-radius: var(--admin-radius-sm);
|
||||
|
||||
&:hover {
|
||||
color: var(--color-white);
|
||||
background-color: var(--el-color-danger);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__active-bar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
<template>
|
||||
<el-dropdown class="user-dropdown px-1" @command="handleCommand">
|
||||
<div class="user-trigger flex items-center gap-2.5 px-2 py-1 rounded-md cursor-pointer">
|
||||
<el-avatar :size="32" :src="userInfo.avatar" />
|
||||
<span class="user-name max-w-[120px] truncate text-sm font-medium text-tx-primary">{{
|
||||
userInfo.name
|
||||
}}</span>
|
||||
<icon name="el-icon-ArrowDown" :size="14" />
|
||||
<el-dropdown class="px-2" @command="handleCommand">
|
||||
<div class="flex items-center">
|
||||
<el-avatar :size="34" :src="userInfo.avatar" />
|
||||
<div class="ml-3 mr-1">{{ userInfo.name }}</div>
|
||||
<icon name="el-icon-ArrowDown" />
|
||||
</div>
|
||||
|
||||
<template #dropdown>
|
||||
@@ -43,13 +41,3 @@ const handleCommand = async (command: string) => {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.user-trigger {
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,56 +1,27 @@
|
||||
<template>
|
||||
|
||||
<main class="main-wrap h-full">
|
||||
|
||||
<main class="main-wrap h-full bg-page">
|
||||
<el-scrollbar>
|
||||
|
||||
<div class="main-stage">
|
||||
|
||||
<page-shell v-if="isRouteShow">
|
||||
|
||||
<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 class="px-2 py-4">
|
||||
<router-view v-if="isRouteShow" v-slot="{ Component, route }">
|
||||
<keep-alive :include="includeList" :max="20">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</div>
|
||||
|
||||
</el-scrollbar>
|
||||
|
||||
</main>
|
||||
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import PageShell from '@/components/page-shell/index.vue'
|
||||
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
|
||||
import useTabsStore from '@/stores/modules/multipleTabs'
|
||||
|
||||
import useSettingStore from '@/stores/modules/setting'
|
||||
|
||||
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const tabsStore = useTabsStore()
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const isRouteShow = computed(() => appStore.isRouteShow)
|
||||
|
||||
const includeList = computed(() => (settingStore.openMultipleTabs ? tabsStore.getCacheTabList : []))
|
||||
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
|
||||
@@ -100,7 +100,7 @@ import theme_light from '@/assets/images/theme_white.png'
|
||||
import useSettingStore from '@/stores/modules/setting'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const predefineColors = ref(['#06b6d4', '#10b981', '#0891b2', '#6366f1', '#f59e0b', '#ef4444', '#64748b'])
|
||||
const predefineColors = ref(['#409EFF', '#28C76F', '#EA5455', '#FF9F43', '#01CFE8', '#4A5DFF'])
|
||||
const sideThemeList = [
|
||||
{
|
||||
type: 'dark',
|
||||
|
||||
@@ -68,42 +68,32 @@ const themeClass = computed(() => `theme-${props.theme}`)
|
||||
.el-menu {
|
||||
:deep(.el-menu-item) {
|
||||
&.is-active {
|
||||
@apply bg-primary;
|
||||
box-shadow: inset 3px 0 0 var(--el-color-primary-light-3);
|
||||
@apply bg-primary border-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-menu--collapse) {
|
||||
.el-sub-menu.is-active .el-sub-menu__title {
|
||||
@apply bg-primary #{!important};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.theme-light {
|
||||
:deep(.el-menu) {
|
||||
.el-menu-item {
|
||||
border-color: transparent;
|
||||
|
||||
&.is-active {
|
||||
@apply bg-primary-light-9;
|
||||
color: var(--el-color-primary);
|
||||
font-weight: 600;
|
||||
box-shadow: inset 3px 0 0 var(--el-color-primary);
|
||||
@apply bg-primary-light-9 border-r-2 border-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu-item:hover,
|
||||
.el-sub-menu__title:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu {
|
||||
border-right: none;
|
||||
|
||||
&:not(.el-menu--collapse) {
|
||||
width: var(--aside-width);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,5 @@
|
||||
<template>
|
||||
<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-menu
|
||||
:routes="routes"
|
||||
@@ -35,19 +25,17 @@ const appStore = useAppStore()
|
||||
const isCollapsed = computed(() => {
|
||||
if (appStore.isMobile) {
|
||||
return false
|
||||
} else {
|
||||
return appStore.isCollapsed
|
||||
}
|
||||
return appStore.isCollapsed
|
||||
})
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const sideTheme = computed(() => settingStore.sideTheme)
|
||||
const userStore = useUserStore()
|
||||
const config = computed(() => appStore.config)
|
||||
|
||||
const routes = computed(() => userStore.routes)
|
||||
|
||||
const showBrandStrip = computed(() => !settingStore.showLogo && !isCollapsed.value)
|
||||
|
||||
const sideStyle = computed(() => {
|
||||
return sideTheme.value == 'dark'
|
||||
? {
|
||||
@@ -55,7 +43,6 @@ const sideStyle = computed(() => {
|
||||
}
|
||||
: ''
|
||||
})
|
||||
|
||||
const menuProp = computed(() => {
|
||||
return {
|
||||
backgroundColor: sideTheme.value == 'dark' ? settingStore.sideDarkColor : '',
|
||||
@@ -63,7 +50,6 @@ const menuProp = computed(() => {
|
||||
activeTextColor: sideTheme.value == 'dark' ? 'var(--el-color-white)' : ''
|
||||
}
|
||||
})
|
||||
|
||||
const handleSelect = () => {
|
||||
if (appStore.isMobile) {
|
||||
appStore.toggleCollapsed(true)
|
||||
@@ -75,8 +61,7 @@ const handleSelect = () => {
|
||||
.side {
|
||||
position: relative;
|
||||
z-index: 999;
|
||||
@apply h-full flex flex-col;
|
||||
border-right: 1px solid var(--admin-sidebar-border);
|
||||
background-color: var(--side-dark-color, var(--sidebar-dark-bg));
|
||||
@apply border-r border-br-light h-full flex flex-col;
|
||||
background-color: var(--side-dark-color, var(--el-bg-color));
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,298 +0,0 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
/* 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);
|
||||
}
|
||||
}
|
||||
+31
-46
@@ -1,58 +1,43 @@
|
||||
:root.dark {
|
||||
color-scheme: dark;
|
||||
|
||||
--table-header-bg-color: rgba(6, 182, 212, 0.12);
|
||||
--sidebar-dark-bg: #030712;
|
||||
--sidebar-dark-hover: rgba(255, 255, 255, 0.05);
|
||||
--sidebar-dark-active: rgba(6, 182, 212, 0.2);
|
||||
|
||||
--admin-surface-elevated: #111827;
|
||||
--admin-surface-muted: rgba(17, 24, 39, 0.88);
|
||||
--admin-surface-glass: rgba(17, 24, 39, 0.86);
|
||||
|
||||
--el-bg-color-page: #030712;
|
||||
--el-bg-color: #0f172a;
|
||||
--el-bg-color-overlay: #1e293b;
|
||||
--el-text-color-primary: #f1f5f9;
|
||||
--el-text-color-regular: #cbd5e1;
|
||||
--el-text-color-secondary: #94a3b8;
|
||||
--el-text-color-placeholder: #64748b;
|
||||
--el-text-color-disabled: #475569;
|
||||
--el-border-color-darker: #64748b;
|
||||
--el-border-color-dark: #475569;
|
||||
--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;
|
||||
--table-header-bg-color: var(--el-bg-color);
|
||||
--el-bg-color-page: #0a0a0a;
|
||||
--el-bg-color: #1d2124;
|
||||
--el-bg-color-overlay: #1d1e1f;
|
||||
--el-text-color-primary: #e5eaf3;
|
||||
--el-text-color-regular: #cfd3dc;
|
||||
--el-text-color-secondary: #a3a6ad;
|
||||
--el-text-color-placeholder: #8d9095;
|
||||
--el-text-color-disabled: #6c6e72;
|
||||
--el-border-color-darker: #636466;
|
||||
--el-border-color-dark: #58585b;
|
||||
--el-border-color: #4c4d4f;
|
||||
--el-border-color-light: #414243;
|
||||
--el-border-color-lighter: #363637;
|
||||
--el-border-color-extra-light: #2b2b2c;
|
||||
--el-fill-color-darker: #424243;
|
||||
--el-fill-color-dark: #39393a;
|
||||
--el-fill-color: #303030;
|
||||
--el-fill-color-light: #262727;
|
||||
--el-fill-color-lighter: #1d1d1d;
|
||||
--el-fill-color-extra-light: #191919;
|
||||
--el-fill-color-blank: var(--el-bg-color);
|
||||
|
||||
--el-mask-color: rgba(2, 6, 23, 0.78);
|
||||
--el-mask-color-extra-light: rgba(2, 6, 23, 0.42);
|
||||
|
||||
--el-box-shadow: 0 4px 24px rgba(0, 0, 0, 0.28);
|
||||
--el-box-shadow-light: 0 2px 16px rgba(0, 0, 0, 0.22);
|
||||
--el-box-shadow-lighter: 0 1px 4px rgba(0, 0, 0, 0.16);
|
||||
--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);
|
||||
|
||||
--el-mask-color: rgba(0, 0, 0, 0.8);
|
||||
--el-mask-color-extra-light: rgba(0, 0, 0, 0.3);
|
||||
--el-box-shadow: 0px 12px 32px 4px rgba(0, 0, 0, 0.36), 0px 8px 20px rgba(0, 0, 0, 0.72);
|
||||
--el-box-shadow-light: 0px 0px 12px rgba(0, 0, 0, 0.72);
|
||||
--el-box-shadow-lighter: 0px 0px 6px rgba(0, 0, 0, 0.72);
|
||||
--el-box-shadow-dark: 0px 16px 48px 16px rgba(0, 0, 0, 0.72), 0px 12px 32px #000000,
|
||||
0px 8px 16px -8px #000000 !important;
|
||||
/* wangeditor主题 */
|
||||
--w-e-textarea-bg-color: var(--el-bg-color);
|
||||
--w-e-textarea-color: var(--el-text-color-primary);
|
||||
--w-e-textarea-border-color: var(--el-border-color);
|
||||
--w-e-textarea-slight-border-color: var(--el-border-color-light);
|
||||
--w-e-textarea-slight-color: var(--el-border-color);
|
||||
--w-e-textarea-slight-bg-color: var(--el-bg-color-page);
|
||||
/* --w-e-textarea-selected-border-color: #b4d5ff;
|
||||
--w-e-textarea-handler-bg-color: #4290f7; */
|
||||
--w-e-toolbar-color: var(--el-text-color-primary);
|
||||
--w-e-toolbar-bg-color: var(--el-bg-color);
|
||||
--w-e-toolbar-active-color: var(--el-text-color-primary);
|
||||
|
||||
+17
-237
@@ -1,20 +1,14 @@
|
||||
:root {
|
||||
/* Messages & notifications */
|
||||
// 确保消息提示在最上层
|
||||
.el-message {
|
||||
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 {
|
||||
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 {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -25,14 +19,11 @@
|
||||
.el-dialog {
|
||||
--el-dialog-content-font-size: var(--el-font-size-base);
|
||||
--el-dialog-margin-top: 50px;
|
||||
--el-dialog-border-radius: var(--admin-radius-lg);
|
||||
max-width: calc(100vw - 32px);
|
||||
max-width: calc(100vw - 30px);
|
||||
flex: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: var(--admin-radius-lg);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
box-shadow: var(--el-box-shadow-dark);
|
||||
border-radius: 5px;
|
||||
|
||||
&.body-padding .el-dialog__body {
|
||||
padding: 0;
|
||||
@@ -40,158 +31,50 @@
|
||||
|
||||
.el-dialog__body {
|
||||
flex: 1;
|
||||
padding: 16px 20px 20px;
|
||||
padding: 15px 20px;
|
||||
}
|
||||
|
||||
.el-dialog__header {
|
||||
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 {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Drawer */
|
||||
.el-drawer {
|
||||
--el-drawer-padding-primary: 16px;
|
||||
|
||||
&__header {
|
||||
margin-bottom: 0;
|
||||
padding: 14px 18px;
|
||||
padding: 13px 16px;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__title {
|
||||
@apply text-tx-primary;
|
||||
}
|
||||
|
||||
&__body {
|
||||
padding: 16px 18px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 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;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
th.el-table__cell {
|
||||
background-color: var(--table-header-bg-color);
|
||||
}
|
||||
}
|
||||
|
||||
.el-table__cell {
|
||||
padding: 12px 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Form controls */
|
||||
.el-input-group__prepend {
|
||||
background-color: var(--el-fill-color-light);
|
||||
border-color: var(--el-border-color);
|
||||
background-color: var(--el-fill-color-blank);
|
||||
}
|
||||
|
||||
.el-checkbox {
|
||||
--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 {
|
||||
&.theme-light {
|
||||
.el-menu {
|
||||
@@ -200,14 +83,12 @@
|
||||
@apply bg-primary-light-9 border-primary border-r-2;
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu-item:hover,
|
||||
.el-sub-menu__title:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.theme-dark {
|
||||
.el-menu {
|
||||
.el-menu-item {
|
||||
@@ -220,120 +101,52 @@
|
||||
}
|
||||
|
||||
.el-message-box {
|
||||
--el-messagebox-width: 380px;
|
||||
--el-messagebox-border-radius: var(--admin-radius-lg);
|
||||
--el-messagebox-width: 350px;
|
||||
}
|
||||
|
||||
.el-date-editor {
|
||||
--el-date-editor-datetimerange-width: 380px;
|
||||
|
||||
.el-range-input {
|
||||
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-hover-link-text-color: var(--el-color-success-light-3);
|
||||
}
|
||||
|
||||
.el-button--info {
|
||||
--el-button-hover-link-text-color: var(--el-color-info-light-3);
|
||||
}
|
||||
|
||||
.el-button--warning {
|
||||
--el-button-hover-link-text-color: var(--el-color-warning-light-3);
|
||||
}
|
||||
|
||||
.el-button--danger {
|
||||
--el-button-hover-link-text-color: var(--el-color-danger-light-3);
|
||||
}
|
||||
|
||||
.el-image__error {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.el-tabs__nav-wrap::after {
|
||||
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 {
|
||||
&__breadcrumb {
|
||||
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-select,
|
||||
.el-textarea {
|
||||
@apply shadow-primary-light-8;
|
||||
|
||||
box-shadow: 0 0 0 0 var(--tw-shadow-color);
|
||||
|
||||
&:focus-within {
|
||||
box-shadow: 0 0 0 2px var(--tw-shadow-color);
|
||||
border-radius: var(--el-input-border-radius, var(--el-border-radius-base));
|
||||
transition: box-shadow ease 0.15s;
|
||||
transition: box-shadow ease 0.1s;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,7 +156,6 @@
|
||||
border-radius: var(--el-checkbox-border-radius);
|
||||
|
||||
box-shadow: 0 0 0 0 var(--tw-shadow-color);
|
||||
|
||||
&:active {
|
||||
box-shadow: 0 0 0 2px var(--tw-shadow-color);
|
||||
transition: box-shadow ease 0s;
|
||||
@@ -361,61 +173,29 @@
|
||||
.el-form-item.is-error .el-checkbox {
|
||||
@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) {
|
||||
.el-pagination > .el-pagination__jump {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.el-pagination > .el-pagination__sizes {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-button {
|
||||
// 防止被tailwindcss默认样式覆盖
|
||||
background-color: var(--el-button-bg-color, var(--el-color-white));
|
||||
|
||||
//覆盖el-button的点击样式
|
||||
&:focus {
|
||||
color: var(--el-button-text-color);
|
||||
border-color: var(--el-button-border-color);
|
||||
background-color: var(--el-button-bg-color);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--el-button-hover-text-color);
|
||||
border-color: var(--el-button-hover-border-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,7 +1,5 @@
|
||||
@import 'var.css';
|
||||
@import 'dark.css';
|
||||
@import 'tailwind.css';
|
||||
@import 'element.scss';
|
||||
@import 'admin-shell.scss';
|
||||
@import 'admin-pages.scss';
|
||||
@import 'dark.css';
|
||||
@import 'var.css';
|
||||
@import 'tailwind.css';
|
||||
@import 'public.scss';
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
body {
|
||||
@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 {
|
||||
@apply text-tx-secondary text-xs leading-6 mt-1;
|
||||
}
|
||||
@@ -16,51 +12,7 @@ body {
|
||||
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 .bar {
|
||||
@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;
|
||||
}
|
||||
|
||||
+35
-85
@@ -1,99 +1,49 @@
|
||||
:root {
|
||||
/* Typography */
|
||||
--el-font-family: theme(fontFamily.sans);
|
||||
--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-menu-base-level-padding: 16px;
|
||||
--el-menu-level-padding: 26px;
|
||||
--el-font-size-large: 16px;
|
||||
--el-font-size-medium: 15px;
|
||||
--el-font-size-base: 14px;
|
||||
--el-font-size-small: 13px;
|
||||
--el-font-size-extra-small: 12px;
|
||||
|
||||
/* 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-page: #eef6fb;
|
||||
--el-bg-color-page: #f6f6f6;
|
||||
--el-bg-color-overlay: #ffffff;
|
||||
--el-text-color-primary: #0b1220;
|
||||
--el-text-color-regular: #334155;
|
||||
--el-text-color-secondary: #64748b;
|
||||
--el-text-color-placeholder: #94a3b8;
|
||||
--el-text-color-disabled: #cbd5e1;
|
||||
--el-border-color: rgba(6, 182, 212, 0.12);
|
||||
--el-border-color-light: rgba(6, 182, 212, 0.08);
|
||||
--el-border-color-lighter: rgba(15, 23, 42, 0.06);
|
||||
--el-border-color-extra-light: rgba(15, 23, 42, 0.04);
|
||||
--el-border-color-dark: #cbd5e1;
|
||||
--el-border-color-darker: #94a3b8;
|
||||
--el-fill-color: #f1f5f9;
|
||||
--el-fill-color-light: #f8fafc;
|
||||
--el-fill-color-lighter: #fafbfc;
|
||||
--el-fill-color-extra-light: #fcfdfe;
|
||||
--el-fill-color-dark: #e2e8f0;
|
||||
--el-fill-color-darker: #cbd5e1;
|
||||
--el-text-color-primary: #333333;
|
||||
--el-text-color-regular: #666666;
|
||||
--el-text-color-secondary: #999999;
|
||||
--el-text-color-placeholder: #a8abb2;
|
||||
--el-text-color-disabled: #c0c4cc;
|
||||
--el-border-color: #dcdfe6;
|
||||
--el-border-color-light: #e4e7ed;
|
||||
--el-border-color-lighter: #ebeef5;
|
||||
--el-border-color-extra-light: #f2f2f2;
|
||||
--el-border-color-dark: #d4d7de;
|
||||
--el-border-color-darker: #cdd0d6;
|
||||
--el-fill-color: #f0f2f5;
|
||||
--el-fill-color-light: #f8f8f8;
|
||||
--el-fill-color-lighter: #fafafa;
|
||||
--el-fill-color-extra-light: #fafcff;
|
||||
--el-fill-color-dark: #ebedf0;
|
||||
--el-fill-color-darker: #e6e8eb;
|
||||
--el-fill-color-blank: #ffffff;
|
||||
|
||||
--el-mask-color: rgba(6, 13, 24, 0.55);
|
||||
--el-mask-color-extra-light: rgba(6, 13, 24, 0.1);
|
||||
|
||||
--el-box-shadow: 0 4px 24px rgba(6, 182, 212, 0.08), 0 12px 40px rgba(15, 23, 42, 0.06);
|
||||
--el-box-shadow-light: 0 2px 16px rgba(6, 182, 212, 0.1);
|
||||
--el-box-shadow-lighter: 0 1px 4px rgba(15, 23, 42, 0.04);
|
||||
--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;
|
||||
/* 过亮会盖住抽屉/弹窗下的内容;Element Loading 与部分蒙层共用此变量 */
|
||||
--el-mask-color: rgba(255, 255, 255, 0.5);
|
||||
--el-mask-color-extra-light: rgba(255, 255, 255, 0.22);
|
||||
-el-box-shadow: 0px 12px 32px 4px rgba(0, 0, 0, 0.04), 0px 8px 20px rgba(0, 0, 0, 0.08);
|
||||
--el-box-shadow-light: 0px 0px 12px rgba(0, 0, 0, 0.12);
|
||||
--el-box-shadow-lighter: 0px 0px 6px rgba(0, 0, 0, 0.12);
|
||||
--el-box-shadow-dark: 0px 16px 48px 16px rgba(0, 0, 0, 0.08), 0px 12px 32px rgba(0, 0, 0, 0.12),
|
||||
0px 8px 16px -8px rgba(0, 0, 0, 0.16);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<template>
|
||||
<div class="change-password flex flex-col">
|
||||
<div class="change-password-backdrop" aria-hidden="true"></div>
|
||||
<div class="flex-1 flex items-center justify-center relative z-[1] px-4">
|
||||
<div class="change-password-card">
|
||||
<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>
|
||||
<div class="flex-1 flex items-center justify-center">
|
||||
<div class="change-password-card bg-body rounded-md px-10 py-10 w-[480px]">
|
||||
<div class="text-center text-2xl font-medium mb-2">首次登录</div>
|
||||
<div class="text-center text-gray-500 text-sm mb-8">为了您的账号安全,请修改初始密码</div>
|
||||
|
||||
<el-form ref="formRef" :model="formData" size="large" :rules="rules">
|
||||
<el-form-item prop="password">
|
||||
@@ -118,25 +117,7 @@ const { isLock, lockFn: lockSubmit } = useLockFn(handleSubmit)
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.change-password {
|
||||
position: relative;
|
||||
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);
|
||||
background-image: url('./images/login_bg.png');
|
||||
@apply min-h-screen bg-no-repeat bg-center bg-cover;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,41 +1,33 @@
|
||||
<template>
|
||||
<div class="login-v2">
|
||||
<div class="login-v2__mesh" aria-hidden="true"></div>
|
||||
<div class="login-v2__layout">
|
||||
<aside class="login-v2__brand">
|
||||
<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 class="login flex flex-col">
|
||||
<div class="flex-1 flex items-center justify-center">
|
||||
<div class="login-card flex rounded-md overflow-hidden">
|
||||
<div class="flex-1 h-full hidden md:inline-block">
|
||||
<image-contain :src="config.login_image" :width="400" height="100%" />
|
||||
</div>
|
||||
</aside>
|
||||
<div
|
||||
class="login-form bg-body flex flex-col justify-center px-10 py-10 md:w-[420px] w-[380px] flex-none mx-auto"
|
||||
>
|
||||
<div class="text-center text-3xl font-medium mb-8">{{ config.web_name }}</div>
|
||||
|
||||
<section class="login-v2__panel">
|
||||
<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">
|
||||
<!-- 企业微信自动授权中 -->
|
||||
<div v-if="wxWorkAutoLogin" class="text-center py-10">
|
||||
<el-icon class="is-loading mb-4" :size="40" color="var(--el-color-primary)">
|
||||
<Loading />
|
||||
</el-icon>
|
||||
<div class="text-tx-secondary">企业微信授权登录中...</div>
|
||||
<div class="text-gray-500">企业微信授权登录中...</div>
|
||||
</div>
|
||||
|
||||
<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-button value="account">账号登录</el-radio-button>
|
||||
<el-radio-button value="wxwork">企业微信</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
|
||||
<!-- 账号密码登录 -->
|
||||
<template v-if="loginMode === 'account'">
|
||||
<el-form ref="formRef" :model="formData" size="large" :rules="rules">
|
||||
<el-form-item prop="account">
|
||||
@@ -66,34 +58,29 @@
|
||||
<div class="mb-5">
|
||||
<el-checkbox v-model="remAccount" label="记住账号"></el-checkbox>
|
||||
</div>
|
||||
<el-button
|
||||
class="login-v2__submit"
|
||||
type="primary"
|
||||
size="large"
|
||||
:loading="isLock"
|
||||
@click="lockLogin"
|
||||
>
|
||||
进入工作台
|
||||
<el-button type="primary" size="large" :loading="isLock" @click="lockLogin">
|
||||
登录
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
<!-- 企业微信扫码登录(非企业微信内浏览器) -->
|
||||
<template v-if="loginMode === 'wxwork'">
|
||||
<div class="wxwork-qrcode-wrap">
|
||||
<div v-if="wxWorkLoading" class="text-center py-10">
|
||||
<el-icon class="is-loading" :size="32" color="var(--el-color-primary)">
|
||||
<Loading />
|
||||
</el-icon>
|
||||
<div class="mt-2 text-tx-secondary text-sm">加载企业微信扫码...</div>
|
||||
<div class="mt-2 text-gray-400 text-sm">加载企业微信扫码...</div>
|
||||
</div>
|
||||
<div v-else id="wxwork_qrcode_container" class="wxwork-qrcode"></div>
|
||||
</div>
|
||||
<div class="text-center text-sm text-tx-secondary mt-4">
|
||||
<div class="text-center text-sm text-gray-400 mt-4">
|
||||
请使用企业微信扫描二维码登录
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<layout-footer />
|
||||
</div>
|
||||
@@ -299,178 +286,31 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.login-v2 {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
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 {
|
||||
background-image: url('./images/login_bg.png');
|
||||
@apply min-h-screen bg-no-repeat bg-center bg-cover;
|
||||
.login-card {
|
||||
height: auto;
|
||||
min-height: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.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 {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 360px;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.wxwork-qrcode {
|
||||
width: 320px;
|
||||
height: 360px;
|
||||
width: 340px;
|
||||
height: 400px;
|
||||
overflow: hidden;
|
||||
|
||||
:deep(iframe) {
|
||||
width: 320px !important;
|
||||
height: 360px !important;
|
||||
width: 340px !important;
|
||||
height: 400px !important;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-transparency: reduce) {
|
||||
.login-v2__glass {
|
||||
background: #ffffff;
|
||||
backdrop-filter: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<template>
|
||||
<div>
|
||||
<admin-page-filter-panel>
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-alert
|
||||
type="warning"
|
||||
title="温馨提示:用于管理网站的分类,只可添加到一级"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
</admin-page-filter-panel>
|
||||
|
||||
<admin-page-data-panel v-loading="pager.loading">
|
||||
<template #toolbar>
|
||||
</el-card>
|
||||
<el-card class="!border-none mt-4" shadow="never" v-loading="pager.loading">
|
||||
<div>
|
||||
<el-button
|
||||
class="mb-4"
|
||||
v-perms="['article.articleCate/add']"
|
||||
type="primary"
|
||||
@click="handleAdd()"
|
||||
@@ -21,7 +21,7 @@
|
||||
</template>
|
||||
新增
|
||||
</el-button>
|
||||
</template>
|
||||
</div>
|
||||
<el-table size="large" :data="pager.lists">
|
||||
<el-table-column label="栏目名称" prop="name" min-width="120" />
|
||||
<el-table-column label="文章数" prop="article_count" min-width="120" />
|
||||
@@ -58,10 +58,10 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<div class="flex justify-end mt-4">
|
||||
<pagination v-model="pager" @change="getLists" />
|
||||
</template>
|
||||
</admin-page-data-panel>
|
||||
</div>
|
||||
</el-card>
|
||||
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<admin-page-filter-panel>
|
||||
<div class="article-lists">
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-form ref="formRef" class="mb-[-16px]" :model="queryParams" :inline="true">
|
||||
<el-form-item class="w-[280px]" label="文章标题">
|
||||
<el-input
|
||||
@@ -33,25 +33,24 @@
|
||||
<el-button @click="resetParams">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</admin-page-filter-panel>
|
||||
|
||||
<admin-page-data-panel v-loading="pager.loading">
|
||||
<template #toolbar>
|
||||
</el-card>
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<div>
|
||||
<router-link
|
||||
v-perms="['article.article/add', 'article.article/add:edit']"
|
||||
:to="{
|
||||
path: getRoutePath('article.article/add:edit')
|
||||
}"
|
||||
>
|
||||
<el-button type="primary">
|
||||
<el-button type="primary" class="mb-4">
|
||||
<template #icon>
|
||||
<icon name="el-icon-Plus" />
|
||||
</template>
|
||||
发布文章
|
||||
</el-button>
|
||||
</router-link>
|
||||
</template>
|
||||
<el-table size="large" :data="pager.lists">
|
||||
</div>
|
||||
<el-table size="large" v-loading="pager.loading" :data="pager.lists">
|
||||
<el-table-column label="ID" prop="id" min-width="80" />
|
||||
<el-table-column label="封面" min-width="100">
|
||||
<template #default="{ row }">
|
||||
@@ -117,10 +116,10 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<div class="flex justify-end mt-4">
|
||||
<pagination v-model="pager" @change="getLists" />
|
||||
</template>
|
||||
</admin-page-data-panel>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup name="articleLists">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="asset-resource-container">
|
||||
<!-- 搜索区域 -->
|
||||
<admin-page-filter-panel>
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-form class="ls-form" :model="searchData" inline>
|
||||
<el-form-item class="w-[280px]" label="标题">
|
||||
<el-input
|
||||
@@ -28,25 +28,25 @@
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</admin-page-filter-panel>
|
||||
</el-card>
|
||||
|
||||
<!-- 列表区域 -->
|
||||
<admin-page-data-panel v-loading="loading">
|
||||
<template #toolbar>
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<div class="mb-4 flex items-center gap-2">
|
||||
<el-button type="primary" @click="handleAdd">上传资源</el-button>
|
||||
<el-button type="danger" :disabled="selectedIds.length === 0" @click="handleBatchDelete">
|
||||
批量删除
|
||||
</el-button>
|
||||
<span v-if="selectedIds.length > 0" class="text-sm text-gray-500">已选 {{ selectedIds.length }} 条</span>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
|
||||
<el-tabs v-model="queryParams.type" @tab-change="handleTabChange">
|
||||
<el-tab-pane label="图片" name="1"></el-tab-pane>
|
||||
<el-tab-pane label="视频" name="2"></el-tab-pane>
|
||||
<el-tab-pane label="语音" name="3"></el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<el-table :data="tableData" @selection-change="handleSelectionChange">
|
||||
<el-table :data="tableData" v-loading="loading" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" />
|
||||
<!-- <el-table-column prop="id" label="ID" width="80" /> -->
|
||||
<el-table-column prop="title" label="标题" min-width="80" />
|
||||
@@ -71,7 +71,7 @@
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<template #footer>
|
||||
<div class="mt-4 flex justify-end">
|
||||
<el-pagination
|
||||
v-model:current-page="queryParams.page_no"
|
||||
v-model:page-size="queryParams.page_size"
|
||||
@@ -80,8 +80,8 @@
|
||||
@size-change="getList"
|
||||
@current-change="getList"
|
||||
/>
|
||||
</template>
|
||||
</admin-page-data-panel>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 新增/编辑资源弹窗 -->
|
||||
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑资源' : '新增分发资源'" width="600px" destroy-on-close>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="asset-user-container">
|
||||
<!-- 搜索区域 -->
|
||||
<admin-page-filter-panel>
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-form class="ls-form" :model="searchData" inline>
|
||||
<el-form-item class="w-[280px]" label="手机号">
|
||||
<el-input
|
||||
@@ -16,15 +16,15 @@
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</admin-page-filter-panel>
|
||||
</el-card>
|
||||
|
||||
<!-- 列表区域 -->
|
||||
<admin-page-data-panel v-loading="loading">
|
||||
<template #toolbar>
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<div class="mb-4">
|
||||
<el-button type="primary" @click="handleAdd">新增账号</el-button>
|
||||
</template>
|
||||
|
||||
<el-table :data="tableData">
|
||||
</div>
|
||||
|
||||
<el-table :data="tableData" v-loading="loading">
|
||||
<!-- <el-table-column prop="id" label="ID" width="80" /> -->
|
||||
<el-table-column prop="phone" label="手机号" />
|
||||
<el-table-column label="备注" min-width="200">
|
||||
@@ -63,7 +63,7 @@
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<template #footer>
|
||||
<div class="mt-4 flex justify-end">
|
||||
<el-pagination
|
||||
v-model:current-page="queryParams.page_no"
|
||||
v-model:page-size="queryParams.page_size"
|
||||
@@ -72,8 +72,8 @@
|
||||
@size-change="getList"
|
||||
@current-change="getList"
|
||||
/>
|
||||
</template>
|
||||
</admin-page-data-panel>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 编辑/新增弹窗 -->
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px" destroy-on-close>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<admin-page-filter-panel>
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-form ref="formRef" class="mb-[-16px]" :model="queryParams" :inline="true">
|
||||
<el-form-item class="w-[280px]" label="用户信息">
|
||||
<el-input
|
||||
@@ -37,9 +37,8 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</admin-page-filter-panel>
|
||||
|
||||
<admin-page-data-panel>
|
||||
</el-card>
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<el-table size="large" v-loading="pager.loading" :data="pager.lists">
|
||||
<el-table-column label="头像" min-width="100">
|
||||
<template #default="{ row }">
|
||||
@@ -68,10 +67,10 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<div class="flex justify-end mt-4">
|
||||
<pagination v-model="pager" @change="getLists" />
|
||||
</template>
|
||||
</admin-page-data-panel>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup name="consumerLists">
|
||||
|
||||
+197
-25
@@ -631,7 +631,7 @@
|
||||
<el-descriptions-item label="上次医护">{{ detailData.prev_staff || '—' }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="服务渠道">{{ detailData.service_channel || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="服务套餐">{{ formatServicePackage(detailData.service_package) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="服务套餐">{{ detailServicePackageText }}</el-descriptions-item>
|
||||
<el-descriptions-item label="费用类别">{{ feeTypeText(detailData.fee_type) }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item v-if="showInternalCost" label="内部成本">
|
||||
@@ -780,7 +780,18 @@
|
||||
class="po-panel border-gray-100 mt-4"
|
||||
>
|
||||
<template #header>
|
||||
<span class="font-medium text-[15px]">操作日志</span>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="font-medium text-[15px]">操作日志</span>
|
||||
<el-button
|
||||
v-if="canAddPrescriptionOrderLog()"
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
@click="openAddLogDialog"
|
||||
>
|
||||
新增日志
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-timeline v-if="detailLogs.length" class="mt-2 pl-2">
|
||||
<el-timeline-item
|
||||
@@ -800,16 +811,92 @@
|
||||
</el-timeline>
|
||||
<el-empty v-else description="暂无操作日志" :image-size="64" />
|
||||
</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>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="PrescriptionOrderDetailDrawer">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { Refresh, Loading, Search, Van } from '@element-plus/icons-vue'
|
||||
import {
|
||||
prescriptionOrderDetail,
|
||||
prescriptionOrderLogs,
|
||||
prescriptionOrderAddLog,
|
||||
prescriptionOrderLogisticsTrace,
|
||||
prescriptionOrderLogisticsJdUpdate,
|
||||
prescriptionOrderPaidPayOrders
|
||||
@@ -831,6 +918,7 @@ import {
|
||||
consumerRxAuditTag,
|
||||
expressCompanyLabel,
|
||||
logActionText,
|
||||
auditStatusText,
|
||||
formatPayOrderSource,
|
||||
normalizeBizPhone,
|
||||
recipientVsPrescriptionPhoneMismatch,
|
||||
@@ -841,7 +929,10 @@ import {
|
||||
analyzeLogisticsPayloadUrgent,
|
||||
parseLogisticsTracePayload,
|
||||
canUpdateAmount,
|
||||
formatDietaryTaboo
|
||||
formatDietaryTaboo,
|
||||
type ServicePackageOption,
|
||||
normalizeServicePackageOptions,
|
||||
formatServicePackageLabels
|
||||
} from './prescription-order-utils'
|
||||
|
||||
const props = withDefaults(
|
||||
@@ -873,6 +964,7 @@ const emit = defineEmits<{
|
||||
(e: 'view-prescription'): void
|
||||
(e: 'test-gancao-preview'): void
|
||||
(e: 'view-patient'): void
|
||||
(e: 'detail-changed'): void
|
||||
}>()
|
||||
|
||||
const userStore = useUserStore()
|
||||
@@ -1088,36 +1180,20 @@ const detailFullAddress = computed(() => {
|
||||
})
|
||||
|
||||
// ─── 服务套餐字典 ───
|
||||
const servicePackageOptions = ref<Array<{ name: string; value: string }>>([])
|
||||
const servicePackageOptions = ref<ServicePackageOption[]>([])
|
||||
|
||||
async function loadServicePackageOptions() {
|
||||
try {
|
||||
const data: any = await getDictData({ type: 'server_order' })
|
||||
servicePackageOptions.value = (data?.server_order || []).filter((item: any) => item.status !== 0)
|
||||
servicePackageOptions.value = normalizeServicePackageOptions(data?.server_order)
|
||||
} catch {
|
||||
servicePackageOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
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('、')
|
||||
}
|
||||
const detailServicePackageText = computed(() =>
|
||||
formatServicePackageLabels(detailData.value?.service_package, servicePackageOptions.value)
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
loadServicePackageOptions()
|
||||
@@ -1142,6 +1218,102 @@ 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' }]
|
||||
}
|
||||
|
||||
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[]) {
|
||||
if (!diagnosisId) {
|
||||
|
||||
@@ -137,11 +137,22 @@ export function logActionText(act: string) {
|
||||
patch_rx_patient: '处方患者信息',
|
||||
update_amount: '修改订单金额',
|
||||
complete: '完成订单',
|
||||
refund: '退款'
|
||||
refund: '退款',
|
||||
manual_log: '手工备注',
|
||||
assign_assistant: '改派医助',
|
||||
add_pay_order: '补齐支付单',
|
||||
set_ship_mode: '发货类型'
|
||||
}
|
||||
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 }) {
|
||||
const createType = String(row?.create_type || '')
|
||||
@@ -357,3 +368,85 @@ export function formatDietaryTaboo(raw: unknown): string {
|
||||
}
|
||||
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"
|
||||
:params="prescriptionOrderExportParams"
|
||||
:page-size="pager.size"
|
||||
export-hint="导出范围与上方筛选一致(履约状态、创建时间及其他条件均会生效)。含「自媒体渠道(挂号渠道来源)」:优先取该单关联处方登记的挂号;无则诊单下同患者挂号取 id 最大的一条(与前台挂号选择的记录一致);业绩侧栏带渠道筛选导出时与同页列表高亮挂号同源。「服务套餐」按字典 server_order 解析展示。「关联收款记录」与详情侧栏同源(已支付/已退款/待审核),每笔两行展示(摘要行+明细行),多笔空行分隔,单元格自动换行。「签收日期」与详情/业绩看板同源,仅读物流库(轨迹/签收时间):导出不再实时查快递100,速度只取决于数据库;签收时间由 `tcm:backfill-sign-time` 命令与物流自动更新定时任务落库,刚发货尚未同步的单子会暂时为空,待下次回填/定时任务刷新后显示。"
|
||||
export-hint="导出范围与上方筛选一致(履约状态、创建时间及其他条件均会生效)。含「自媒体渠道(挂号渠道来源)」:优先取该单关联处方登记的挂号;无则诊单下同患者挂号取 id 最大的一条(与前台挂号选择的记录一致);业绩侧栏带渠道筛选导出时与同页列表高亮挂号同源。「服务套餐」按字典 server_order 解析展示。「处方」导出主方/辅方药材明细;「主方/辅方服用方式、天数」与详情侧栏、处方笺同口径(天数优先取订单 medication_days,缺省回退处方 usage_days / 辅方 aux_usage)。「关联收款记录」与详情侧栏同源(已支付/已退款/待审核),每笔两行展示(摘要行+明细行),多笔空行分隔,单元格自动换行。「签收日期」与详情/业绩看板同源,仅读物流库(轨迹/签收时间):导出不再实时查快递100,速度只取决于数据库;签收时间由 `tcm:backfill-sign-time` 命令与物流自动更新定时任务落库,刚发货尚未同步的单子会暂时为空,待下次回填/定时任务刷新后显示。"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
@@ -623,6 +623,7 @@
|
||||
@view-prescription="detailData && openPrescriptionView(detailData)"
|
||||
@test-gancao-preview="testGancaoPreviewFromDetail"
|
||||
@view-patient="openDiagnosisPatientDetailFromOrder"
|
||||
@detail-changed="getLists"
|
||||
>
|
||||
<template #header-extra="{ detail }">
|
||||
<div class="flex items-center gap-2 ml-4 shrink-0">
|
||||
@@ -1024,10 +1025,11 @@
|
||||
class="w-full"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in servicePackageOptions"
|
||||
v-for="item in editServicePackageSelectOptions"
|
||||
:key="item.value"
|
||||
:label="item.name"
|
||||
:value="item.value"
|
||||
:disabled="item.status === 0"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
@@ -2225,7 +2227,11 @@ import {
|
||||
canUpdateAmount,
|
||||
formatDietaryTaboo,
|
||||
type SlipFormulaType,
|
||||
type SlipAuxUsageForm
|
||||
type SlipAuxUsageForm,
|
||||
type ServicePackageOption,
|
||||
normalizeServicePackageOptions,
|
||||
parseServicePackageValues,
|
||||
mergeServicePackageSelectOptions
|
||||
} from './components/prescription-order-utils'
|
||||
import { useListTimeFilter } from '@/hooks/useListTimeFilter'
|
||||
import {
|
||||
@@ -2414,8 +2420,8 @@ async function submitReassign() {
|
||||
// 省市区数据
|
||||
const regionOptions = ref([])
|
||||
|
||||
// 服务套餐选项
|
||||
const servicePackageOptions = ref<Array<{ name: string; value: string }>>([])
|
||||
// 服务套餐选项(含已停用项,便于编辑时回显历史值)
|
||||
const servicePackageOptions = ref<ServicePackageOption[]>([])
|
||||
|
||||
/** 筛选:开方医生、诊单医助(与列表接口 doctor_id / assistant_id 一致) */
|
||||
const doctorOptions = ref<Array<{ id: number; name: string }>>([])
|
||||
@@ -2536,8 +2542,7 @@ const loadRegionData = async () => {
|
||||
const loadServicePackageOptions = async () => {
|
||||
try {
|
||||
const data = await getDictData({ type: 'server_order' })
|
||||
servicePackageOptions.value = (data?.server_order || []).filter((item: any) => item.status !== 0)
|
||||
console.log('服务套餐选项已加载:', servicePackageOptions.value.length)
|
||||
servicePackageOptions.value = normalizeServicePackageOptions(data?.server_order)
|
||||
} catch (error) {
|
||||
console.error('加载服务套餐选项失败:', error)
|
||||
servicePackageOptions.value = []
|
||||
@@ -3195,10 +3200,17 @@ async function onDetailShipModeChange(mode: string | number | boolean | undefine
|
||||
}
|
||||
}
|
||||
|
||||
function canAddPayOrderRow(row: { fulfillment_status?: number }) {
|
||||
// 已发货(5) / 已签收(6) 状态可补齐支付单
|
||||
function canAddPayOrderRow(row: {
|
||||
fulfillment_status?: number
|
||||
amount?: number | string
|
||||
linked_pay_paid_total?: number | string
|
||||
}) {
|
||||
// 已发货(5) / 已签收(6) 状态可补齐支付单;总金额已付清则不允许
|
||||
const fs = Number(row.fulfillment_status)
|
||||
return fs === 5 || fs === 6
|
||||
if (fs !== 5 && fs !== 6) return false
|
||||
const orderAmount = Math.round((Number(row.amount) || 0) * 100) / 100
|
||||
const paidTotal = Math.round((Number(row.linked_pay_paid_total) || 0) * 100) / 100
|
||||
return paidTotal < orderAmount
|
||||
}
|
||||
|
||||
function canCompleteRow(row: { fulfillment_status?: number; payment_slip_audit_status?: number }) {
|
||||
@@ -3449,6 +3461,11 @@ const editForm = reactive({
|
||||
diagnosis_creator_dept_path: ''
|
||||
})
|
||||
|
||||
/** 编辑弹窗下拉:字典项 + 当前已选但字典中缺失的兜底项 */
|
||||
const editServicePackageSelectOptions = computed(() =>
|
||||
mergeServicePackageSelectOptions(servicePackageOptions.value, editForm.service_package)
|
||||
)
|
||||
|
||||
/** 诊单创建人部门:拆成面包屑。多部门为「;」分隔;路径内为「 / 」含父级(与后端 buildDeptPath 一致) */
|
||||
const editDiagnosisCreatorDeptBreadcrumbs = computed(() => {
|
||||
const raw = String(editForm.diagnosis_creator_dept_path || '').trim()
|
||||
@@ -3681,18 +3698,7 @@ async function openEdit(row: {
|
||||
editForm.dose_unit = d.dose_unit || '剂'
|
||||
editForm.prev_staff = d.prev_staff || ''
|
||||
editForm.service_channel = d.service_channel || ''
|
||||
// 处理服务套餐:如果是字符串,转换为数组
|
||||
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.service_package = parseServicePackageValues(d.service_package)
|
||||
editForm.express_company = String(d.express_company || 'auto') || 'auto'
|
||||
editForm.tracking_number = d.tracking_number || ''
|
||||
editForm.fee_type = Number(d.fee_type) || 3
|
||||
@@ -4509,7 +4515,18 @@ async function loadAddPayOrderAvailable(diagnosisId: number, currentLinkedIds: n
|
||||
}
|
||||
}
|
||||
|
||||
function openAddPayOrder(row: { id: number; diagnosis_id?: number; pay_order_ids?: number[] }) {
|
||||
function openAddPayOrder(row: {
|
||||
id: number
|
||||
diagnosis_id?: number
|
||||
pay_order_ids?: number[]
|
||||
fulfillment_status?: number
|
||||
amount?: number | string
|
||||
linked_pay_paid_total?: number | string
|
||||
}) {
|
||||
if (!canAddPayOrderRow(row)) {
|
||||
feedback.msgWarning('订单总金额与已付金额一致,无需补齐支付单')
|
||||
return
|
||||
}
|
||||
addPayOrderRowId.value = row.id
|
||||
addPayOrderForm.add_mode = 'create'
|
||||
addPayOrderForm.order_type = 3
|
||||
|
||||
@@ -1139,7 +1139,7 @@
|
||||
<el-descriptions-item label="上次医护">{{ detailData.prev_staff || '—' }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="服务渠道">{{ detailData.service_channel || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="服务套餐">{{ formatServicePackage(detailData.service_package) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="服务套餐">{{ detailServicePackageText }}</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="内部成本">
|
||||
@@ -1525,10 +1525,11 @@
|
||||
class="w-full"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in servicePackageOptions"
|
||||
v-for="item in editServicePackageSelectOptions"
|
||||
:key="item.value"
|
||||
:label="item.name"
|
||||
:value="item.value"
|
||||
:disabled="item.status === 0"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
@@ -2573,7 +2574,14 @@ import {
|
||||
getDoctors,
|
||||
getAssistants
|
||||
} from '@/api/tcm'
|
||||
import { formatDietaryTaboo } from './components/prescription-order-utils'
|
||||
import {
|
||||
formatDietaryTaboo,
|
||||
type ServicePackageOption,
|
||||
normalizeServicePackageOptions,
|
||||
parseServicePackageValues,
|
||||
mergeServicePackageSelectOptions,
|
||||
formatServicePackageLabels
|
||||
} from './components/prescription-order-utils'
|
||||
import html2canvas from 'html2canvas'
|
||||
import { jsPDF } from 'jspdf'
|
||||
import { getDictData } from '@/api/app'
|
||||
@@ -2669,7 +2677,7 @@ const canViewFinanceFields = () => {
|
||||
const regionOptions = ref([])
|
||||
|
||||
// 服务套餐选项
|
||||
const servicePackageOptions = ref<Array<{ name: string; value: string }>>([])
|
||||
const servicePackageOptions = ref<ServicePackageOption[]>([])
|
||||
|
||||
/** 筛选:开方医生、诊单医助(与列表接口 doctor_id / assistant_id 一致) */
|
||||
const doctorOptions = ref<Array<{ id: number; name: string }>>([])
|
||||
@@ -2790,8 +2798,7 @@ const loadRegionData = async () => {
|
||||
const loadServicePackageOptions = async () => {
|
||||
try {
|
||||
const data = await getDictData({ type: 'server_order' })
|
||||
servicePackageOptions.value = (data?.server_order || []).filter((item: any) => item.status !== 0)
|
||||
console.log('服务套餐选项已加载:', servicePackageOptions.value.length)
|
||||
servicePackageOptions.value = normalizeServicePackageOptions(data?.server_order)
|
||||
} catch (error) {
|
||||
console.error('加载服务套餐选项失败:', error)
|
||||
servicePackageOptions.value = []
|
||||
@@ -3199,28 +3206,6 @@ function feeTypeText(t: number | undefined) {
|
||||
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) {
|
||||
if (s === 1) return '已通过'
|
||||
if (s === 2) return '已驳回'
|
||||
@@ -3473,6 +3458,10 @@ const detailVisible = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
const detailData = ref<Record<string, any> | null>(null)
|
||||
|
||||
const detailServicePackageText = computed(() =>
|
||||
formatServicePackageLabels(detailData.value?.service_package, servicePackageOptions.value)
|
||||
)
|
||||
|
||||
// --- 订单可视化审批履约流程逻辑 ---
|
||||
const workflowActiveStep = computed(() => {
|
||||
if (!detailData.value) return 0
|
||||
@@ -4060,6 +4049,10 @@ const editForm = reactive({
|
||||
diagnosis_creator_dept_path: ''
|
||||
})
|
||||
|
||||
const editServicePackageSelectOptions = computed(() =>
|
||||
mergeServicePackageSelectOptions(servicePackageOptions.value, editForm.service_package)
|
||||
)
|
||||
|
||||
/** 诊单创建人部门:拆成面包屑。多部门为「;」分隔;路径内为「 / 」含父级(与后端 buildDeptPath 一致) */
|
||||
const editDiagnosisCreatorDeptBreadcrumbs = computed(() => {
|
||||
const raw = String(editForm.diagnosis_creator_dept_path || '').trim()
|
||||
@@ -4310,18 +4303,7 @@ async function openEdit(row: {
|
||||
editForm.dose_unit = d.dose_unit || '剂'
|
||||
editForm.prev_staff = d.prev_staff || ''
|
||||
editForm.service_channel = d.service_channel || ''
|
||||
// 处理服务套餐:如果是字符串,转换为数组
|
||||
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.service_package = parseServicePackageValues(d.service_package)
|
||||
editForm.express_company = String(d.express_company || 'auto') || 'auto'
|
||||
editForm.tracking_number = d.tracking_number || ''
|
||||
editForm.fee_type = Number(d.fee_type) || 3
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<admin-page-filter-panel>
|
||||
<div class="code-generation">
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-form class="mb-[-16px]" :model="formData" inline>
|
||||
<el-form-item class="w-[280px]" label="表名称">
|
||||
<el-input v-model="formData.table_name" clearable @keyup.enter="resetPage" />
|
||||
@@ -13,8 +13,8 @@
|
||||
<el-button @click="resetParams">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</admin-page-filter-panel>
|
||||
<admin-page-data-panel>
|
||||
</el-card>
|
||||
<el-card class="!border-none mt-4" shadow="never" v-loading="pager.loading">
|
||||
<div class="flex">
|
||||
<data-table
|
||||
v-perms="['tools.generator/selectTable']"
|
||||
@@ -126,10 +126,10 @@
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="flex justify-end mt-4">
|
||||
<pagination v-model="pager" @change="getLists" />
|
||||
</template>
|
||||
</admin-page-data-panel>
|
||||
</div>
|
||||
</el-card>
|
||||
<code-preview
|
||||
v-if="previewState.show"
|
||||
v-model="previewState.show"
|
||||
|
||||
@@ -866,7 +866,7 @@ onUnmounted(() => {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
background: linear-gradient(145deg, #0d9488 0%, #0891b2 100%);
|
||||
background: linear-gradient(145deg, #6366f1 0%, #8b5cf6 100%);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<div class="error">
|
||||
<div class="error-panel">
|
||||
<div>
|
||||
<slot name="content">
|
||||
<div class="error-code">{{ code }}</div>
|
||||
</slot>
|
||||
<div class="error-title">{{ title }}</div>
|
||||
<el-button v-if="showBtn" type="primary" size="large" @click="router.go(-1)">
|
||||
<div class="text-lg text-tx-secondary mt-7 mb-7">{{ title }}</div>
|
||||
<el-button v-if="showBtn" type="primary" @click="router.go(-1)">
|
||||
{{ second }} 秒后返回上一页
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -43,38 +43,16 @@ onUnmounted(() => {
|
||||
<style lang="scss" scoped>
|
||||
.error {
|
||||
text-align: center;
|
||||
min-height: 100vh;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: var(--el-bg-color-page);
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.error-panel {
|
||||
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;
|
||||
.error-code {
|
||||
@apply text-primary;
|
||||
font-size: 150px;
|
||||
}
|
||||
.el-button {
|
||||
width: 176px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="fans-management">
|
||||
<!-- 搜索区域 -->
|
||||
<admin-page-filter-panel>
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-form class="ls-form" :model="formData" inline>
|
||||
<el-form-item class="w-[280px]" label="姓名">
|
||||
<el-input
|
||||
@@ -36,14 +36,14 @@
|
||||
<el-button @click="resetParams">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</admin-page-filter-panel>
|
||||
</el-card>
|
||||
|
||||
<!-- 列表区域 -->
|
||||
<admin-page-data-panel v-loading="pager.loading">
|
||||
<template #toolbar>
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<div class="mb-4">
|
||||
<el-button type="primary" @click="handleAdd">新增粉丝</el-button>
|
||||
</template>
|
||||
<el-table :data="pager.lists" size="large">
|
||||
</div>
|
||||
<el-table :data="pager.lists" size="large" v-loading="pager.loading">
|
||||
<el-table-column label="ID" prop="id" width="70" />
|
||||
<el-table-column label="姓名" prop="name" min-width="100" />
|
||||
<el-table-column label="手机号" prop="phone" min-width="130" />
|
||||
@@ -81,10 +81,10 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<div class="flex mt-4 justify-end">
|
||||
<pagination v-model="pager" @change="getLists" />
|
||||
</template>
|
||||
</admin-page-data-panel>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 新增/编辑粉丝弹窗 -->
|
||||
<el-dialog
|
||||
|
||||
@@ -3704,7 +3704,7 @@ function getYejiDeptComboOption(tb: YejiTable) {
|
||||
const cats = rows.map(r =>
|
||||
r.dept_name.length > 10 ? `${r.dept_name.slice(0, 10)}…` : r.dept_name
|
||||
)
|
||||
const palette = ['#64748b', '#38bdf8', '#0d9488']
|
||||
const palette = ['#64748b', '#38bdf8', '#6366f1']
|
||||
const series: any[] = [
|
||||
{
|
||||
name: '进线',
|
||||
@@ -4029,8 +4029,8 @@ onMounted(async () => {
|
||||
|
||||
.yeji-page {
|
||||
min-width: 0;
|
||||
--yj-brand: #0d9488;
|
||||
--yj-brand-soft: #f0fdfa;
|
||||
--yj-brand: #6366f1;
|
||||
--yj-brand-soft: #eef2ff;
|
||||
--yj-teal: #0ea5e9;
|
||||
--yj-teal-soft: #f0f9ff;
|
||||
--yj-accent: #f43f5e;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<admin-page-filter-panel>
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-alert
|
||||
type="warning"
|
||||
title="温馨提示:用户账户变动记录"
|
||||
@@ -38,9 +38,9 @@
|
||||
<el-button @click="resetParams">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</admin-page-filter-panel>
|
||||
<admin-page-data-panel v-loading="pager.loading">
|
||||
<el-table size="large" :data="pager.lists">
|
||||
</el-card>
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<el-table size="large" v-loading="pager.loading" :data="pager.lists">
|
||||
<el-table-column label="用户账号" prop="account" min-width="100" />
|
||||
<el-table-column label="用户昵称" min-width="160">
|
||||
<template #default="{ row }">
|
||||
@@ -71,10 +71,10 @@
|
||||
<el-table-column label="来源单号" prop="source_sn" min-width="100" />
|
||||
<el-table-column label="记录时间" prop="create_time" min-width="120" />
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<div class="flex justify-end mt-4">
|
||||
<pagination v-model="pager" @change="getLists" />
|
||||
</template>
|
||||
</admin-page-data-panel>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup name="balanceDetail">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<admin-page-filter-panel>
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-alert
|
||||
type="warning"
|
||||
title="温馨提示:用户充值记录"
|
||||
@@ -54,9 +54,9 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</admin-page-filter-panel>
|
||||
<admin-page-data-panel v-loading="pager.loading">
|
||||
<el-table size="large" :data="pager.lists">
|
||||
</el-card>
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<el-table size="large" v-loading="pager.loading" :data="pager.lists">
|
||||
<el-table-column label="用户信息" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<div class="flex items-center">
|
||||
@@ -104,10 +104,10 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<div class="flex justify-end mt-4">
|
||||
<pagination v-model="pager" @change="getLists" />
|
||||
</template>
|
||||
</admin-page-data-panel>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup name="rechargeRecord">
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
<admin-page-filter-panel>
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-form ref="formRef" class="mb-[-16px] mt-[16px]" :model="queryParams" :inline="true">
|
||||
<el-form-item class="w-[280px]" label="退款单号">
|
||||
<el-input
|
||||
@@ -69,8 +69,8 @@
|
||||
/> -->
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</admin-page-filter-panel>
|
||||
<admin-page-data-panel>
|
||||
</el-card>
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<el-tabs v-model="activeTab" @tab-change="handleTabChange">
|
||||
<el-tab-pane
|
||||
v-for="(item, index) in tabLists"
|
||||
@@ -78,7 +78,7 @@
|
||||
:name="index"
|
||||
:key="index"
|
||||
>
|
||||
<el-table size="large" :data="pager.lists">
|
||||
<el-table size="large" v-loading="pager.loading" :data="pager.lists">
|
||||
<el-table-column label="退款单号" prop="sn" min-width="190" />
|
||||
<el-table-column label="用户信息" min-width="160">
|
||||
<template #default="{ row }">
|
||||
@@ -140,10 +140,10 @@
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end mt-4">
|
||||
<pagination v-model="pager" @change="getLists" />
|
||||
</template>
|
||||
</admin-page-data-panel>
|
||||
</div>
|
||||
</el-card>
|
||||
<refund-log v-model="showRefundLog" :refund-id="selectRefundId" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<div>
|
||||
<admin-page-filter-panel>
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-alert
|
||||
type="warning"
|
||||
title="温馨提示:平台配置在各个场景下的通知发送方式和内容模板"
|
||||
:closable="false"
|
||||
show-icon
|
||||
></el-alert>
|
||||
</admin-page-filter-panel>
|
||||
<admin-page-data-panel v-loading="pager.loading">
|
||||
</el-card>
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<el-tabs v-model="tabsActive" @tab-change="getLists">
|
||||
<el-tab-pane
|
||||
v-for="(item, index) in tabsMap"
|
||||
@@ -18,7 +18,7 @@
|
||||
lazy
|
||||
></el-tab-pane>
|
||||
</el-tabs>
|
||||
<el-table size="large" :data="pager.lists" >
|
||||
<el-table size="large" :data="pager.lists" v-loading="pager.loading">
|
||||
<el-table-column label="通知场景" prop="scene_name" min-width="120" />
|
||||
<el-table-column label="通知类型" prop="type_desc" min-width="160" />
|
||||
<el-table-column label="短信通知" min-width="80">
|
||||
@@ -44,7 +44,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</admin-page-data-panel>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup name="notice">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<admin-page-data-panel v-loading="state.loading">
|
||||
<el-card class="!border-none" shadow="never" v-loading="state.loading">
|
||||
<el-table size="large" :data="state.lists">
|
||||
<el-table-column label="短信渠道" prop="name" min-width="120" />
|
||||
<el-table-column label="状态" min-width="120">
|
||||
@@ -22,7 +22,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</admin-page-data-panel>
|
||||
</el-card>
|
||||
<edit-popup ref="editRef" @success="getLists" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -33,11 +33,13 @@ import EditPopup from './edit.vue'
|
||||
|
||||
const editRef = shallowRef<InstanceType<typeof EditPopup>>()
|
||||
|
||||
// 列表数据
|
||||
const state = reactive({
|
||||
loading: false,
|
||||
lists: []
|
||||
})
|
||||
|
||||
// 获取存储引擎列表数据
|
||||
const getLists = async () => {
|
||||
try {
|
||||
state.loading = true
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<!-- 订单列表 -->
|
||||
<template>
|
||||
<div>
|
||||
<admin-page-filter-panel>
|
||||
<div class="order-list">
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<!-- 搜索表单 -->
|
||||
<el-form class="ls-form" :model="queryParams" inline>
|
||||
<el-form-item class="w-[280px]" label="订单号">
|
||||
@@ -106,10 +106,10 @@
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</admin-page-filter-panel>
|
||||
</el-card>
|
||||
|
||||
<!-- Tab 筛选 + 今日收益 + 数据表格 -->
|
||||
<admin-page-data-panel v-loading="pager.loading">
|
||||
<!-- Tab 筛选 + 今日收益 -->
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<el-tabs v-model="patientAssociationTab" @tab-change="handleTabChange">
|
||||
<el-tab-pane label="全部" name="" />
|
||||
@@ -122,7 +122,10 @@
|
||||
<span class="text-gray-400">({{ todayRevenue.count }} 笔)</span>
|
||||
</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">
|
||||
<el-button type="primary" :disabled="!selectedOrderIds.length" @click="openAssignDialog">
|
||||
将创建人指给医助
|
||||
@@ -133,6 +136,7 @@
|
||||
</div>
|
||||
<el-table
|
||||
ref="orderTableRef"
|
||||
v-loading="pager.loading"
|
||||
:data="pager.lists"
|
||||
row-key="id"
|
||||
size="large"
|
||||
@@ -279,10 +283,10 @@
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end mt-4">
|
||||
<pagination v-model="pager" @change="getLists" />
|
||||
</template>
|
||||
</admin-page-data-panel>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 详情弹窗 -->
|
||||
<el-dialog
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<admin-page-filter-panel>
|
||||
<div class="department">
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-form ref="formRef" class="mb-[-16px]" :model="queryParams" :inline="true">
|
||||
<el-form-item class="w-[280px]" label="部门名称" prop="name">
|
||||
<el-input
|
||||
@@ -22,21 +22,22 @@
|
||||
<el-button @click="resetParams">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</admin-page-filter-panel>
|
||||
|
||||
<admin-page-data-panel v-loading="loading">
|
||||
<template #toolbar>
|
||||
</el-card>
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<div>
|
||||
<el-button v-perms="['dept.dept/add']" type="primary" @click="handleAdd()">
|
||||
<template #icon>
|
||||
<icon name="el-icon-Plus" />
|
||||
</template>
|
||||
新增
|
||||
</el-button>
|
||||
<el-button @click="handleExpand">展开/折叠</el-button>
|
||||
</template>
|
||||
<el-button @click="handleExpand"> 展开/折叠 </el-button>
|
||||
</div>
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
class="mt-4"
|
||||
size="large"
|
||||
v-loading="loading"
|
||||
:data="lists"
|
||||
row-key="id"
|
||||
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
||||
@@ -67,6 +68,7 @@
|
||||
}}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="排序" prop="sort" min-width="100" />
|
||||
<el-table-column label="更新时间" prop="update_time" min-width="180" />
|
||||
<el-table-column label="操作" width="160" fixed="right">
|
||||
@@ -99,7 +101,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</admin-page-data-panel>
|
||||
</el-card>
|
||||
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -118,18 +120,14 @@ let isExpand = false
|
||||
const loading = ref(false)
|
||||
const lists = ref<any[]>([])
|
||||
const queryParams = reactive({
|
||||
name: '',
|
||||
status: ''
|
||||
status: '',
|
||||
name: ''
|
||||
})
|
||||
const showEdit = ref(false)
|
||||
|
||||
const getLists = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
lists.value = await deptLists(queryParams)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
lists.value = await deptLists(queryParams)
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
const resetParams = () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<admin-page-filter-panel>
|
||||
<div class="post-lists">
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-form ref="formRef" class="mb-[-16px]" :model="queryParams" :inline="true">
|
||||
<el-form-item class="w-[280px]" label="岗位编码">
|
||||
<el-input
|
||||
@@ -31,18 +31,17 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</admin-page-filter-panel>
|
||||
|
||||
<admin-page-data-panel v-loading="pager.loading">
|
||||
<template #toolbar>
|
||||
</el-card>
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<div>
|
||||
<el-button v-perms="['dept.jobs/add']" type="primary" @click="handleAdd()">
|
||||
<template #icon>
|
||||
<icon name="el-icon-Plus" />
|
||||
</template>
|
||||
新增
|
||||
</el-button>
|
||||
</template>
|
||||
<el-table size="large" :data="pager.lists">
|
||||
</div>
|
||||
<el-table class="mt-4" size="large" v-loading="pager.loading" :data="pager.lists">
|
||||
<el-table-column label="岗位编码" prop="code" min-width="100" />
|
||||
<el-table-column label="岗位名称" prop="name" min-width="100" />
|
||||
<el-table-column label="排序" prop="sort" min-width="100" />
|
||||
@@ -76,10 +75,10 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<div class="flex justify-end mt-4">
|
||||
<pagination v-model="pager" @change="getLists" />
|
||||
</template>
|
||||
</admin-page-data-panel>
|
||||
</div>
|
||||
</el-card>
|
||||
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1275,7 +1275,7 @@ onUnmounted(() => {
|
||||
|
||||
.float-action.edit {
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #0d9488 0%, #0f766e 100%);
|
||||
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
|
||||
}
|
||||
|
||||
.float-action.edit:hover,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<admin-page-filter-panel>
|
||||
<div class="admin">
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-form class="mb-[-16px]" :model="formData" inline>
|
||||
<el-form-item class="w-[280px]" label="管理员账号">
|
||||
<el-input
|
||||
@@ -40,78 +40,77 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</admin-page-filter-panel>
|
||||
|
||||
<admin-page-data-panel v-loading="pager.loading">
|
||||
<template #toolbar>
|
||||
<el-button v-perms="['auth.admin/add']" type="primary" @click="handleAdd">
|
||||
<template #icon>
|
||||
<icon name="el-icon-Plus" />
|
||||
</template>
|
||||
新增
|
||||
</el-button>
|
||||
</template>
|
||||
<el-table :data="pager.lists" size="large">
|
||||
<el-table-column label="ID" prop="id" min-width="60" />
|
||||
<el-table-column label="头像" min-width="100">
|
||||
<template #default="{ row }">
|
||||
<el-avatar :size="50" :src="row.avatar"></el-avatar>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="账号" prop="account" min-width="100" />
|
||||
<el-table-column label="名称" prop="name" min-width="100" />
|
||||
<el-table-column
|
||||
label="角色"
|
||||
prop="role_name"
|
||||
min-width="100"
|
||||
show-tooltip-when-overflow
|
||||
/>
|
||||
<el-table-column
|
||||
label="部门"
|
||||
prop="dept_name"
|
||||
min-width="100"
|
||||
show-tooltip-when-overflow
|
||||
/>
|
||||
<el-table-column label="创建时间" prop="create_time" min-width="180" />
|
||||
<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="状态" min-width="100" v-perms="['auth.admin/edit']">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
v-if="row.root != 1"
|
||||
v-model="row.disable"
|
||||
:active-value="0"
|
||||
:inactive-value="1"
|
||||
@change="changeStatus(row)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-perms="['auth.admin/edit']"
|
||||
type="primary"
|
||||
link
|
||||
@click="handleEdit(row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.root != 1"
|
||||
v-perms="['auth.admin/delete']"
|
||||
type="danger"
|
||||
link
|
||||
@click="handleDelete(row.id)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<template #footer>
|
||||
</el-card>
|
||||
<el-card v-loading="pager.loading" class="mt-4 !border-none" shadow="never">
|
||||
<el-button v-perms="['auth.admin/add']" type="primary" @click="handleAdd">
|
||||
<template #icon>
|
||||
<icon name="el-icon-Plus" />
|
||||
</template>
|
||||
新增
|
||||
</el-button>
|
||||
<div class="mt-4">
|
||||
<el-table :data="pager.lists" size="large">
|
||||
<el-table-column label="ID" prop="id" min-width="60" />>
|
||||
<el-table-column label="头像" min-width="100">
|
||||
<template #default="{ row }">
|
||||
<el-avatar :size="50" :src="row.avatar"></el-avatar>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="账号" prop="account" min-width="100" />
|
||||
<el-table-column label="名称" prop="name" min-width="100" />
|
||||
<el-table-column
|
||||
label="角色"
|
||||
prop="role_name"
|
||||
min-width="100"
|
||||
show-tooltip-when-overflow
|
||||
/>
|
||||
<el-table-column
|
||||
label="部门"
|
||||
prop="dept_name"
|
||||
min-width="100"
|
||||
show-tooltip-when-overflow
|
||||
/>
|
||||
<el-table-column label="创建时间" prop="create_time" min-width="180" />
|
||||
<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="状态" min-width="100" v-perms="['auth.admin/edit']">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
v-if="row.root != 1"
|
||||
v-model="row.disable"
|
||||
:active-value="0"
|
||||
:inactive-value="1"
|
||||
@change="changeStatus(row)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-perms="['auth.admin/edit']"
|
||||
type="primary"
|
||||
link
|
||||
@click="handleEdit(row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.root != 1"
|
||||
v-perms="['auth.admin/delete']"
|
||||
type="danger"
|
||||
link
|
||||
@click="handleDelete(row.id)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="flex mt-4 justify-end">
|
||||
<pagination v-model="pager" @change="getLists" />
|
||||
</template>
|
||||
</admin-page-data-panel>
|
||||
</div>
|
||||
</el-card>
|
||||
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -126,6 +125,7 @@ import feedback from '@/utils/feedback'
|
||||
import EditPopup from './edit.vue'
|
||||
|
||||
const editRef = shallowRef<InstanceType<typeof EditPopup>>()
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
account: '',
|
||||
name: '',
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
<template>
|
||||
<div>
|
||||
<admin-page-data-panel v-loading="pager.loading">
|
||||
<template #toolbar>
|
||||
<div class="menu-lists">
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<div>
|
||||
<el-button v-perms="['auth.menu/add']" type="primary" @click="handleAdd()">
|
||||
<template #icon>
|
||||
<icon name="el-icon-Plus" />
|
||||
</template>
|
||||
新增
|
||||
</el-button>
|
||||
<el-button @click="handleExpand">展开/折叠</el-button>
|
||||
</template>
|
||||
<el-button @click="handleExpand"> 展开/折叠 </el-button>
|
||||
</div>
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
class="mt-4"
|
||||
size="large"
|
||||
v-loading="pager.loading"
|
||||
:data="pager.lists"
|
||||
row-key="id"
|
||||
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
||||
@@ -85,7 +87,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</admin-page-data-panel>
|
||||
</el-card>
|
||||
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,64 +1,68 @@
|
||||
<template>
|
||||
<div>
|
||||
<admin-page-data-panel v-loading="pager.loading">
|
||||
<template #toolbar>
|
||||
<div class="role-lists">
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<div>
|
||||
<el-button v-perms="['auth.role/add']" type="primary" @click="handleAdd">
|
||||
<template #icon>
|
||||
<icon name="el-icon-Plus" />
|
||||
</template>
|
||||
新增
|
||||
</el-button>
|
||||
</template>
|
||||
<el-table :data="pager.lists" size="large">
|
||||
<el-table-column prop="id" label="ID" min-width="100" />
|
||||
<el-table-column prop="name" label="名称" min-width="150" />
|
||||
<el-table-column
|
||||
prop="desc"
|
||||
label="备注"
|
||||
min-width="150"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column prop="sort" label="排序" min-width="100" />
|
||||
<el-table-column label="数据范围" min-width="140">
|
||||
<template #default="{ row }">
|
||||
{{ dataScopeLabel(row.data_scope) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="num" label="管理员人数" min-width="100" />
|
||||
<el-table-column prop="create_time" label="创建时间" min-width="180" />
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
v-perms="['auth.role/edit']"
|
||||
@click="handleEdit(row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
v-perms="['auth.role/edit']"
|
||||
@click="handleAuth(row)"
|
||||
>
|
||||
分配权限
|
||||
</el-button>
|
||||
<el-button
|
||||
v-perms="['auth.role/delete']"
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete(row.id)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<pagination v-model="pager" @change="getLists" />
|
||||
</template>
|
||||
</admin-page-data-panel>
|
||||
</div>
|
||||
<div class="mt-4" v-loading="pager.loading">
|
||||
<div>
|
||||
<el-table :data="pager.lists" size="large">
|
||||
<el-table-column prop="id" label="ID" min-width="100" />
|
||||
<el-table-column prop="name" label="名称" min-width="150" />
|
||||
<el-table-column
|
||||
prop="desc"
|
||||
label="备注"
|
||||
min-width="150"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column prop="sort" label="排序" min-width="100" />
|
||||
<el-table-column label="数据范围" min-width="140">
|
||||
<template #default="{ row }">
|
||||
{{ dataScopeLabel(row.data_scope) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="num" label="管理员人数" min-width="100" />
|
||||
<el-table-column prop="create_time" label="创建时间" min-width="180" />
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
v-perms="['auth.role/edit']"
|
||||
@click="handleEdit(row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
v-perms="['auth.role/edit']"
|
||||
@click="handleAuth(row)"
|
||||
>
|
||||
分配权限
|
||||
</el-button>
|
||||
<el-button
|
||||
v-perms="['auth.role/delete']"
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete(row.id)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="flex justify-end mt-4">
|
||||
<pagination v-model="pager" @change="getLists" />
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
|
||||
<auth-popup v-if="showAuth" ref="authRef" @success="getLists" @close="showAuth = false" />
|
||||
</div>
|
||||
@@ -111,6 +115,7 @@ const dataScopeLabel = (scope: number | string | null | undefined) => {
|
||||
)
|
||||
}
|
||||
|
||||
// 删除角色
|
||||
const handleDelete = async (id: number) => {
|
||||
await feedback.confirm('确定要删除?')
|
||||
await roleDelete({ id })
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<admin-page-filter-panel>
|
||||
<div class="dict-type">
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-page-header class="mb-4" content="数据管理" @back="$router.back()" />
|
||||
<el-form ref="formRef" class="mb-[-16px]" :model="queryParams" inline>
|
||||
<el-form-item class="w-[280px]" label="字典名称">
|
||||
@@ -28,10 +28,9 @@
|
||||
<el-button @click="resetParams">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</admin-page-filter-panel>
|
||||
|
||||
<admin-page-data-panel v-loading="pager.loading">
|
||||
<template #toolbar>
|
||||
</el-card>
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<div>
|
||||
<el-button
|
||||
v-perms="['setting.dict.dict_data/add']"
|
||||
type="primary"
|
||||
@@ -53,54 +52,58 @@
|
||||
</template>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
<el-table
|
||||
:data="pager.lists"
|
||||
size="large"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column label="ID" prop="id" />
|
||||
<el-table-column label="数据名称" prop="name" min-width="120" />
|
||||
<el-table-column label="数据值" prop="value" min-width="120" />
|
||||
<el-table-column label="状态">
|
||||
<template v-slot="{ row }">
|
||||
<el-tag v-if="row.status == 1">正常</el-tag>
|
||||
<el-tag v-else type="danger">停用</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="备注"
|
||||
prop="remark"
|
||||
min-width="120"
|
||||
show-tooltip-when-overflow
|
||||
/>
|
||||
<el-table-column label="排序" prop="sort" />
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-perms="['setting.dict.dict_data/edit']"
|
||||
link
|
||||
type="primary"
|
||||
@click="handleEdit(row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-perms="['setting.dict.dict_data/delete']"
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete(row.id)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<pagination v-model="pager" @change="getLists" />
|
||||
</template>
|
||||
</admin-page-data-panel>
|
||||
</div>
|
||||
<div class="mt-4" v-loading="pager.loading">
|
||||
<div>
|
||||
<el-table
|
||||
:data="pager.lists"
|
||||
size="large"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column label="ID" prop="id" />
|
||||
<el-table-column label="数据名称" prop="name" min-width="120" />
|
||||
<el-table-column label="数据值" prop="value" min-width="120" />
|
||||
<el-table-column label="状态">
|
||||
<template v-slot="{ row }">
|
||||
<el-tag v-if="row.status == 1">正常</el-tag>
|
||||
<el-tag v-else type="danger">停用</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="备注"
|
||||
prop="remark"
|
||||
min-width="120"
|
||||
show-tooltip-when-overflow
|
||||
/>
|
||||
<el-table-column label="排序" prop="sort" />
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-perms="['setting.dict.dict_data/edit']"
|
||||
link
|
||||
type="primary"
|
||||
@click="handleEdit(row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-perms="['setting.dict.dict_data/delete']"
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete(row.id)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="flex justify-end mt-4">
|
||||
<pagination v-model="pager" @change="getLists" />
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<admin-page-filter-panel>
|
||||
<div class="dict-type">
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-form ref="formRef" class="mb-[-16px]" :model="queryParams" inline>
|
||||
<el-form-item class="w-[280px]" label="字典名称">
|
||||
<el-input v-model="queryParams.name" clearable @keyup.enter="resetPage" />
|
||||
@@ -20,10 +20,9 @@
|
||||
<el-button @click="resetParams">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</admin-page-filter-panel>
|
||||
|
||||
<admin-page-data-panel v-loading="pager.loading">
|
||||
<template #toolbar>
|
||||
</el-card>
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<div>
|
||||
<el-button
|
||||
v-perms="['setting.dict.dict_type/add']"
|
||||
type="primary"
|
||||
@@ -45,65 +44,69 @@
|
||||
</template>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
<el-table
|
||||
:data="pager.lists"
|
||||
size="large"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column label="ID" prop="id" />
|
||||
<el-table-column label="字典名称" prop="name" min-width="120" />
|
||||
<el-table-column label="字典类型" prop="type" min-width="120" />
|
||||
<el-table-column label="状态">
|
||||
<template v-slot="{ row }">
|
||||
<el-tag v-if="row.status == 1">正常</el-tag>
|
||||
<el-tag v-else type="danger">停用</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" prop="remark" show-tooltip-when-overflow />
|
||||
<el-table-column label="创建时间" prop="create_time" min-width="180" />
|
||||
<el-table-column label="操作" width="190" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-perms="['setting.dict.dict_type/edit']"
|
||||
link
|
||||
type="primary"
|
||||
@click="handleEdit(row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-perms="['setting.dict.dict_data/lists']"
|
||||
type="primary"
|
||||
link
|
||||
>
|
||||
<router-link
|
||||
:to="{
|
||||
path: getRoutePath('setting.dict.dict_data/lists'),
|
||||
query: {
|
||||
id: row.id
|
||||
}
|
||||
}"
|
||||
>
|
||||
数据管理
|
||||
</router-link>
|
||||
</el-button>
|
||||
<el-button
|
||||
v-perms="['setting.dict.dict_type/delete']"
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete(row.id)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<pagination v-model="pager" @change="getLists" />
|
||||
</template>
|
||||
</admin-page-data-panel>
|
||||
</div>
|
||||
<div class="mt-4" v-loading="pager.loading">
|
||||
<div>
|
||||
<el-table
|
||||
:data="pager.lists"
|
||||
size="large"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column label="ID" prop="id" />
|
||||
<el-table-column label="字典名称" prop="name" min-width="120" />
|
||||
<el-table-column label="字典类型" prop="type" min-width="120" />
|
||||
<el-table-column label="状态">
|
||||
<template v-slot="{ row }">
|
||||
<el-tag v-if="row.status == 1">正常</el-tag>
|
||||
<el-tag v-else type="danger">停用</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" prop="remark" show-tooltip-when-overflow />
|
||||
<el-table-column label="创建时间" prop="create_time" min-width="180" />
|
||||
<el-table-column label="操作" width="190" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-perms="['setting.dict.dict_type/edit']"
|
||||
link
|
||||
type="primary"
|
||||
@click="handleEdit(row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-perms="['setting.dict.dict_data/lists']"
|
||||
type="primary"
|
||||
link
|
||||
>
|
||||
<router-link
|
||||
:to="{
|
||||
path: getRoutePath('setting.dict.dict_data/lists'),
|
||||
query: {
|
||||
id: row.id
|
||||
}
|
||||
}"
|
||||
>
|
||||
数据管理
|
||||
</router-link>
|
||||
</el-button>
|
||||
<el-button
|
||||
v-perms="['setting.dict.dict_type/delete']"
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete(row.id)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="flex justify-end mt-4">
|
||||
<pagination v-model="pager" @change="getLists" />
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -148,6 +151,7 @@ const handleEdit = async (data: any) => {
|
||||
editRef.value?.setFormData(data)
|
||||
}
|
||||
|
||||
// 删除角色
|
||||
const handleDelete = async (id: any[] | number) => {
|
||||
await feedback.confirm('确定要删除?')
|
||||
await dictTypeDelete({ id })
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="conversion-stats-page">
|
||||
<admin-page-filter-panel>
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-form :inline="true" :model="queryParams" class="stats-filter-form">
|
||||
<el-form-item label="统计维度">
|
||||
<el-segmented
|
||||
@@ -89,7 +89,7 @@
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</admin-page-filter-panel>
|
||||
</el-card>
|
||||
|
||||
<div class="stats-kpi-grid">
|
||||
<div
|
||||
|
||||
@@ -121,6 +121,16 @@
|
||||
end-placeholder="最近挂号结束"
|
||||
@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
|
||||
v-model="formData.latest_appointment_channel_source"
|
||||
placeholder="最近挂号渠道"
|
||||
@@ -173,6 +183,7 @@
|
||||
v-loading="pager.loading"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="goReadonly"
|
||||
@sort-change="handleTableSortChange"
|
||||
:row-class-name="getRowClassName"
|
||||
class="diagnosis-table"
|
||||
stripe
|
||||
@@ -283,7 +294,14 @@
|
||||
<span v-else class="status-unprescribed">未开方</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="未服务天数" width="110" align="center">
|
||||
<el-table-column
|
||||
label="未服务天数"
|
||||
prop="unserved_days"
|
||||
width="110"
|
||||
align="center"
|
||||
sortable="custom"
|
||||
:sort-orders="['descending', 'ascending']"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tooltip
|
||||
v-if="row.last_blood_record_at"
|
||||
@@ -762,6 +780,10 @@ const formData = reactive({
|
||||
latest_appointment_start_date: '' as string,
|
||||
latest_appointment_end_date: '' 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',
|
||||
appointment_date: '' as string,
|
||||
has_appointment: '' as '' | '0' | '1',
|
||||
@@ -849,22 +871,50 @@ function resolvePendingAssignOrderMonthForRequest(): string {
|
||||
return dayjs().format('YYYY-MM')
|
||||
}
|
||||
|
||||
/** 待分配角标 count 请求:与列表同条件,且去掉其它顶部 Tab 残留(如默认「当天挂号」) */
|
||||
function buildPendingAssignCountPayload(): Record<string, unknown> {
|
||||
return buildTcmDiagnosisListRequestPayload({
|
||||
...formData,
|
||||
/** 除顶部 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,
|
||||
pending_assign: 1,
|
||||
...buildSharedDiagnosisFilterPayload(),
|
||||
appointment_date: '',
|
||||
has_appointment: '',
|
||||
pending_booking: '',
|
||||
completed_appointment: '',
|
||||
latest_appointment_start_date: '',
|
||||
latest_appointment_end_date: '',
|
||||
latest_appointment_channel_source: '',
|
||||
pending_assign_order_month: resolvePendingAssignOrderMonthForRequest()
|
||||
} as Record<string, unknown>) as Record<string, unknown>
|
||||
pending_assign: '',
|
||||
pending_assign_order_month: '',
|
||||
pending_assign_keyword: '',
|
||||
sort_unserved_days: '',
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
/** 待分配角标 count 请求:与列表同条件,且去掉其它顶部 Tab 残留(如默认「当天挂号」) */
|
||||
function buildPendingAssignCountPayload(): Record<string, unknown> {
|
||||
return buildTcmDiagnosisListRequestPayload(
|
||||
buildDateCountRequestPayload({
|
||||
pending_assign: 1,
|
||||
has_appointment: '',
|
||||
pending_assign_order_month: resolvePendingAssignOrderMonthForRequest(),
|
||||
pending_assign_keyword: formData.pending_assign_keyword
|
||||
}) as Record<string, unknown>
|
||||
) as Record<string, unknown>
|
||||
}
|
||||
|
||||
const fetchTcmDiagnosisListsForPaging = (req: Record<string, unknown>) =>
|
||||
@@ -893,6 +943,9 @@ function clearSecondaryFiltersWhenPendingAssignWideSearch() {
|
||||
formData.latest_appointment_start_date = ''
|
||||
formData.latest_appointment_end_date = ''
|
||||
formData.latest_appointment_channel_source = ''
|
||||
formData.latest_assign_start_date = ''
|
||||
formData.latest_assign_end_date = ''
|
||||
formData.sort_unserved_days = ''
|
||||
formData.pending_assign_order_month = ''
|
||||
activeTab.value = 'all'
|
||||
if (kw1 !== '') {
|
||||
@@ -994,14 +1047,26 @@ const onPendingAssignOrderMonthChange = async (val: string | null) => {
|
||||
const fetchDateCounts = async () => {
|
||||
try {
|
||||
const [yesterday, dayBefore, today, tomorrow, dayAfter, all, noApt, doneVisit, pending] = await Promise.all([
|
||||
tcmDiagnosisLists({ appointment_date: yesterdayStr.value, page_no: 1, page_size: 1 }),
|
||||
tcmDiagnosisLists({ appointment_date: dayBeforeStr.value, page_no: 1, page_size: 1 }),
|
||||
tcmDiagnosisLists({ appointment_date: todayStr.value, page_no: 1, page_size: 1 }),
|
||||
tcmDiagnosisLists({ appointment_date: tomorrowStr.value, page_no: 1, page_size: 1 }),
|
||||
tcmDiagnosisLists({ appointment_date: dayAfterStr.value, page_no: 1, page_size: 1 }),
|
||||
tcmDiagnosisLists({ page_no: 1, page_size: 1 }),
|
||||
tcmDiagnosisLists({ has_appointment: 0, page_no: 1, page_size: 1 }),
|
||||
tcmDiagnosisLists({ completed_appointment: 1, page_no: 1, page_size: 1 }),
|
||||
tcmDiagnosisLists(
|
||||
buildDateCountRequestPayload({ appointment_date: yesterdayStr.value, has_appointment: '' }) as any
|
||||
),
|
||||
tcmDiagnosisLists(
|
||||
buildDateCountRequestPayload({ appointment_date: dayBeforeStr.value, has_appointment: '' }) as any
|
||||
),
|
||||
tcmDiagnosisLists(
|
||||
buildDateCountRequestPayload({ appointment_date: todayStr.value, has_appointment: '' }) as any
|
||||
),
|
||||
tcmDiagnosisLists(
|
||||
buildDateCountRequestPayload({ appointment_date: tomorrowStr.value, has_appointment: '' }) as any
|
||||
),
|
||||
tcmDiagnosisLists(
|
||||
buildDateCountRequestPayload({ appointment_date: dayAfterStr.value, has_appointment: '' }) as any
|
||||
),
|
||||
tcmDiagnosisLists(buildDateCountRequestPayload() as any),
|
||||
tcmDiagnosisLists(buildDateCountRequestPayload({ has_appointment: 0 }) as any),
|
||||
tcmDiagnosisLists(
|
||||
buildDateCountRequestPayload({ completed_appointment: 1, has_appointment: '' }) as any
|
||||
),
|
||||
tcmDiagnosisLists(buildPendingAssignCountPayload() as any)
|
||||
])
|
||||
dateCounts.value = {
|
||||
@@ -1135,6 +1200,11 @@ const clearLatestAppointmentFilters = () => {
|
||||
formData.latest_appointment_channel_source = ''
|
||||
}
|
||||
|
||||
const clearLatestAssignFilters = () => {
|
||||
formData.latest_assign_start_date = ''
|
||||
formData.latest_assign_end_date = ''
|
||||
}
|
||||
|
||||
const hasLatestAppointmentFilter = () =>
|
||||
!!(
|
||||
formData.latest_appointment_start_date ||
|
||||
@@ -1142,6 +1212,9 @@ const hasLatestAppointmentFilter = () =>
|
||||
formData.latest_appointment_channel_source
|
||||
)
|
||||
|
||||
const hasLatestAssignFilter = () =>
|
||||
!!(formData.latest_assign_start_date || formData.latest_assign_end_date)
|
||||
|
||||
const handleLatestAppointmentFilterChange = () => {
|
||||
if (hasLatestAppointmentFilter()) {
|
||||
formData.appointment_date = ''
|
||||
@@ -1154,6 +1227,27 @@ const handleLatestAppointmentFilterChange = () => {
|
||||
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 desc = String(row?.latest_appointment_channel_source_desc || '').trim()
|
||||
const raw = String(row?.latest_appointment_channel_source || '').trim()
|
||||
@@ -1200,6 +1294,9 @@ const handleReset = () => {
|
||||
formData.latest_appointment_start_date = ''
|
||||
formData.latest_appointment_end_date = ''
|
||||
formData.latest_appointment_channel_source = ''
|
||||
formData.latest_assign_start_date = ''
|
||||
formData.latest_assign_end_date = ''
|
||||
formData.sort_unserved_days = ''
|
||||
formData.diagnosis_confirmed = ''
|
||||
formData.appointment_date = ''
|
||||
formData.has_appointment = ''
|
||||
@@ -2298,6 +2395,11 @@ onUnmounted(() => {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.latest-assign-range {
|
||||
width: 260px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.latest-appointment-channel {
|
||||
width: 170px;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
</span>
|
||||
<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 class="wb-toolbar">
|
||||
@@ -821,8 +821,8 @@ onMounted(() => {
|
||||
<style lang="scss" scoped>
|
||||
.workbench-page {
|
||||
min-height: 100%;
|
||||
padding: 4px 0 24px;
|
||||
background: transparent;
|
||||
padding: 20px 20px 40px;
|
||||
background: linear-gradient(160deg, #eef2ff 0%, #f8fafc 38%, #f1f5f9 100%);
|
||||
}
|
||||
|
||||
.wb-hero {
|
||||
@@ -830,86 +830,56 @@ onMounted(() => {
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
margin-bottom: 24px;
|
||||
padding: 28px 28px;
|
||||
border-radius: var(--admin-radius-xl);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
background: var(--admin-surface-glass);
|
||||
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;
|
||||
gap: 16px;
|
||||
margin-bottom: 22px;
|
||||
padding: 22px 26px;
|
||||
border-radius: 20px;
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.92) 0%, rgba(255, 255, 255, 0.65) 100%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.95);
|
||||
box-shadow: 0 12px 40px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.wb-hero-title {
|
||||
margin: 0 0 8px;
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.03em;
|
||||
background: var(--admin-brand-gradient);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
margin: 0 0 6px;
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.wb-hero-desc {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 14px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.wb-refresh-btn {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
box-shadow: var(--admin-brand-glow);
|
||||
box-shadow: 0 8px 20px rgba(59, 130, 246, 0.25);
|
||||
}
|
||||
|
||||
.wb-section {
|
||||
margin-bottom: 20px;
|
||||
border-radius: var(--admin-radius-xl);
|
||||
background: var(--admin-surface-elevated);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
box-shadow: var(--el-box-shadow);
|
||||
border-radius: 20px;
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
border: 1px solid rgba(255, 255, 255, 0.9);
|
||||
box-shadow: 0 8px 32px rgba(15, 23, 42, 0.05);
|
||||
overflow: hidden;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.wb-section--diagnosis {
|
||||
border-top: 3px solid transparent;
|
||||
border-image: linear-gradient(90deg, #06b6d4, #10b981) 1;
|
||||
border-image: linear-gradient(90deg, #3b82f6, #60a5fa) 1;
|
||||
}
|
||||
|
||||
.wb-section--order {
|
||||
border-top: 3px solid transparent;
|
||||
border-image: linear-gradient(90deg, #0891b2, #6366f1) 1;
|
||||
border-image: linear-gradient(90deg, #8b5cf6, #a78bfa) 1;
|
||||
}
|
||||
|
||||
.wb-section--trend {
|
||||
border-top: 3px solid transparent;
|
||||
border-image: linear-gradient(90deg, #10b981, #06b6d4) 1;
|
||||
border-image: linear-gradient(90deg, #14b8a6, #2dd4bf) 1;
|
||||
}
|
||||
|
||||
.wb-section-head {
|
||||
@@ -919,8 +889,8 @@ onMounted(() => {
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 18px 22px;
|
||||
border-bottom: 1px solid var(--el-border-color-extra-light);
|
||||
background: var(--admin-brand-gradient-soft);
|
||||
border-bottom: 1px solid rgba(226, 232, 240, 0.9);
|
||||
background: linear-gradient(180deg, rgba(248, 250, 252, 0.9) 0%, rgba(255, 255, 255, 0) 100%);
|
||||
}
|
||||
|
||||
.wb-section-title {
|
||||
@@ -933,68 +903,38 @@ onMounted(() => {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 14px;
|
||||
font-size: 22px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.wb-section-icon--blue {
|
||||
background: var(--admin-brand-gradient);
|
||||
box-shadow: var(--admin-brand-glow);
|
||||
background: linear-gradient(135deg, #3b82f6, #2563eb);
|
||||
box-shadow: 0 8px 20px rgba(37, 99, 235, 0.35);
|
||||
}
|
||||
|
||||
.wb-section-icon--violet {
|
||||
background: linear-gradient(135deg, #0891b2, #6366f1);
|
||||
box-shadow: 0 8px 24px rgba(99, 102, 241, 0.25);
|
||||
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
|
||||
box-shadow: 0 8px 20px rgba(124, 58, 237, 0.3);
|
||||
}
|
||||
|
||||
.wb-section-icon--teal {
|
||||
background: linear-gradient(135deg, #10b981, #06b6d4);
|
||||
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);
|
||||
background: linear-gradient(135deg, #14b8a6, #0d9488);
|
||||
box-shadow: 0 8px 20px rgba(13, 148, 136, 0.3);
|
||||
}
|
||||
|
||||
.wb-section-name {
|
||||
font-size: 16px;
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.wb-section-sub {
|
||||
margin-top: 2px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.wb-toolbar {
|
||||
@@ -1028,6 +968,21 @@ onMounted(() => {
|
||||
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 {
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
@@ -1052,6 +1007,17 @@ onMounted(() => {
|
||||
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 {
|
||||
min-height: 112px;
|
||||
}
|
||||
@@ -1114,10 +1080,9 @@ onMounted(() => {
|
||||
.wb-chart-card {
|
||||
height: 100%;
|
||||
padding: 14px 16px 8px;
|
||||
border-radius: var(--admin-radius-lg);
|
||||
background: var(--admin-surface-elevated);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
box-shadow: var(--el-box-shadow-lighter);
|
||||
border-radius: 16px;
|
||||
background: #fafbfc;
|
||||
border: 1px solid #eef0f4;
|
||||
}
|
||||
|
||||
.wb-chart-card-head {
|
||||
|
||||
@@ -77,22 +77,7 @@ module.exports = {
|
||||
mask: 'var(--el-mask-color)'
|
||||
},
|
||||
fontFamily: {
|
||||
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)'
|
||||
sans: ['PingFang SC', 'Arial', 'Hiragino Sans GB', 'Microsoft YaHei', 'sans-serif']
|
||||
},
|
||||
boxShadow: {
|
||||
DEFAULT: 'var(--el-box-shadow)',
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -328,6 +328,20 @@ class PrescriptionOrderController extends BaseAdminController
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为「已发货」订单新增一条关联支付单,并重置支付单审核状态为待审核
|
||||
*/
|
||||
|
||||
@@ -140,6 +140,7 @@ class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
|
||||
}
|
||||
|
||||
$this->applyLatestAppointmentFilters($query, $pendingWideSearch);
|
||||
$this->applyLatestAssignFilters($query, $pendingWideSearch);
|
||||
|
||||
$this->applyPendingAssignBusinessOrderMonthFilter($query);
|
||||
|
||||
@@ -167,30 +168,7 @@ class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
|
||||
}
|
||||
}
|
||||
|
||||
// 按挂号状态优先级排序:已过号(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";
|
||||
}
|
||||
$orderRaw = $this->resolveListOrderRaw($pendingWideSearch);
|
||||
|
||||
$lists = $query
|
||||
->with(['DiagnosisViewRecord'])
|
||||
@@ -571,6 +549,7 @@ class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
|
||||
}
|
||||
|
||||
$this->applyLatestAppointmentFilters($query, $pendingWideSearch);
|
||||
$this->applyLatestAssignFilters($query, $pendingWideSearch);
|
||||
|
||||
// 仅已开方(待分配+关键词检索时不限制)
|
||||
if (!$pendingWideSearch && isset($this->params['only_has_prescription']) && (string) $this->params['only_has_prescription'] === '1') {
|
||||
@@ -644,6 +623,99 @@ class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
|
||||
$query->whereExists("SELECT 1 FROM {$aptTbl} latest_apt WHERE " . implode(' AND ', $conditions));
|
||||
}
|
||||
|
||||
/**
|
||||
* 最近一次成功指派过滤:按 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
|
||||
{
|
||||
$statuses = implode(',', self::EFFECTIVE_APPOINTMENT_STATUSES);
|
||||
|
||||
@@ -820,6 +820,11 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
|
||||
'export_guahao_channel_source' => '自媒体渠道(挂号渠道来源)',
|
||||
'export_medication_form' => '药品形态',
|
||||
'export_prescription_name' => '药方名称',
|
||||
'export_prescription_herbs' => '处方',
|
||||
'export_main_usage' => '主方服用方式',
|
||||
'export_main_usage_days' => '主方天数',
|
||||
'export_aux_usage' => '辅方服用方式',
|
||||
'export_aux_usage_days' => '辅方天数',
|
||||
'export_service_package' => '服务套餐',
|
||||
'export_medication_days' => '天数',
|
||||
'export_amount' => '总金额',
|
||||
@@ -909,6 +914,9 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
|
||||
$wrapWideKeys = [
|
||||
'export_linked_pay_records',
|
||||
'export_prescription_name',
|
||||
'export_prescription_herbs',
|
||||
'export_main_usage',
|
||||
'export_aux_usage',
|
||||
'export_guahao_channel_source',
|
||||
'export_assistant_dept',
|
||||
'export_service_package',
|
||||
@@ -919,6 +927,8 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
|
||||
'export_patient_gender' => 6,
|
||||
'export_patient_age' => 6,
|
||||
'export_medication_days' => 6,
|
||||
'export_main_usage_days' => 8,
|
||||
'export_aux_usage_days' => 8,
|
||||
'export_amount' => 10,
|
||||
'export_paid_amount' => 10,
|
||||
'export_refund_amount' => 10,
|
||||
@@ -927,6 +937,9 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
|
||||
'export_supply_mode' => 10,
|
||||
'export_linked_pay_records' => 52,
|
||||
'export_prescription_name' => 34,
|
||||
'export_prescription_herbs' => 36,
|
||||
'export_main_usage' => 28,
|
||||
'export_aux_usage' => 28,
|
||||
'export_guahao_channel_source' => 22,
|
||||
'export_assistant_dept' => 24,
|
||||
'export_service_package' => 18,
|
||||
|
||||
@@ -2375,6 +2375,123 @@ class PrescriptionOrderLogic
|
||||
->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),
|
||||
* 创建后将支付单链接到业务订单,并将处方/支付审核状态重置为待审核以启动再次审核流程。
|
||||
@@ -3107,6 +3224,210 @@ class PrescriptionOrderLogic
|
||||
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'), '.');
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出用:主方/辅方服用天数(优先业务订单 medication_days,缺省回退处方 usage_days / 辅方 aux_usage)
|
||||
*
|
||||
* @param array<string, mixed> $rx
|
||||
* @param array<string, mixed>|null $auxUsage
|
||||
*/
|
||||
private static function resolveExportUsageDays(array $rx, ?array $auxUsage, $orderMedicationDays, bool $isAux): string
|
||||
{
|
||||
$medDays = $orderMedicationDays;
|
||||
if ($medDays !== null && $medDays !== '' && (int) $medDays > 0) {
|
||||
return (string) (int) $medDays;
|
||||
}
|
||||
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 同字典口径)
|
||||
*
|
||||
@@ -3403,7 +3724,12 @@ class PrescriptionOrderLogic
|
||||
$rxById = [];
|
||||
if ($rxIdList !== []) {
|
||||
$rxRows = Prescription::whereIn('id', $rxIdList)->whereNull('delete_time')
|
||||
->field(['id', 'prescription_type', 'need_decoction', 'dose_unit', 'assistant_id', 'appointment_id', 'prescription_name', 'aux_usage', 'herbs', 'creator_id'])
|
||||
->field([
|
||||
'id', 'prescription_type', 'need_decoction', 'dose_unit', 'assistant_id', 'appointment_id',
|
||||
'prescription_name', 'aux_usage', 'herbs', 'creator_id',
|
||||
'dosage_amount', 'dosage_unit', 'dosage_bag_count', 'times_per_day', 'usage_days',
|
||||
'usage_way', 'usage_time',
|
||||
])
|
||||
->select()
|
||||
->toArray();
|
||||
foreach ($rxRows as $xr) {
|
||||
@@ -3637,6 +3963,39 @@ class PrescriptionOrderLogic
|
||||
}
|
||||
$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'] = '';
|
||||
}
|
||||
$orderMedDays = $item['medication_days'] ?? null;
|
||||
$item['export_main_usage_days'] = self::resolveExportUsageDays($rxArr, $auxUsageNorm, $orderMedDays, false);
|
||||
$item['export_aux_usage_days'] = $auxHerbs !== []
|
||||
? self::resolveExportUsageDays($rxArr, $auxUsageNorm, $orderMedDays, true)
|
||||
: '';
|
||||
|
||||
$item['export_service_package'] = self::formatServicePackageForExport(
|
||||
$item['service_package'] ?? '',
|
||||
$packageNameByValue
|
||||
@@ -3807,27 +4166,7 @@ class PrescriptionOrderLogic
|
||||
|
||||
$doctorId = (int) ($rx['creator_id'] ?? 0);
|
||||
|
||||
$herbs = $rx['herbs'] ?? null;
|
||||
if (\is_string($herbs) && $herbs !== '') {
|
||||
$decoded = json_decode($herbs, true);
|
||||
$herbs = \is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
if (!\is_array($herbs)) {
|
||||
$herbs = [];
|
||||
}
|
||||
|
||||
$mainHerbs = [];
|
||||
$auxHerbs = [];
|
||||
foreach ($herbs as $h) {
|
||||
if (!\is_array($h)) {
|
||||
continue;
|
||||
}
|
||||
if (((string) ($h['formula_type'] ?? '')) === '辅方') {
|
||||
$auxHerbs[] = $h;
|
||||
} else {
|
||||
$mainHerbs[] = $h;
|
||||
}
|
||||
}
|
||||
[$mainHerbs, $auxHerbs] = self::splitPrescriptionHerbsFromRx($rx);
|
||||
|
||||
$lookup = static function (string $ft, array $hs) use ($doctorId, $libByDoctor, $libPublic): string {
|
||||
if ($hs === []) {
|
||||
|
||||
@@ -32,6 +32,11 @@ class PrescriptionOrderValidate extends BaseValidate
|
||||
'remark_assistant' => 'max:500',
|
||||
'action' => 'require|in:approve,reject',
|
||||
'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',
|
||||
'reason' => 'require|max:500',
|
||||
'refund_amount' => 'float|egt:0',
|
||||
@@ -74,6 +79,7 @@ class PrescriptionOrderValidate extends BaseValidate
|
||||
'withdraw' => ['id'],
|
||||
'ship' => ['id', 'tracking_number', 'express_company', 'ship_mode'],
|
||||
'logs' => ['id'],
|
||||
'addLog' => ['id', 'summary'],
|
||||
'paidPayOrders' => ['diagnosis_id'],
|
||||
'addPayOrder' => ['id', 'order_type', 'pay_amount', 'pay_remark'],
|
||||
'linkPayOrder' => ['id', 'pay_order_id'],
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
import t from"./error-D-UZdrhy.js";import{o,q as r,r as a,v as n,D as c,s}from"./.pnpm-BGZW0UGg.js";import"./index-CeIwrh_6.js";const p="/admin/assets/no_perms-jDxcYpYC.png",i={class:"error404"},x=o({__name:"403",setup(m){return(_,e)=>(r(),a("div",i,[n(t,{code:"403",title:"您的账号权限不足,请联系管理员添加权限!","show-btn":!1},{content:c(()=>[...e[0]||(e[0]=[s("div",{class:"flex justify-center"},[s("img",{class:"w-[150px] h-[150px]",src:p,alt:""})],-1)])]),_:1})]))}});export{x as default};
|
||||
@@ -1 +0,0 @@
|
||||
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};
|
||||
@@ -0,0 +1 @@
|
||||
import e from"./error-D-UZdrhy.js";import{o,q as r,r as t,v as s}from"./.pnpm-BGZW0UGg.js";import"./index-CeIwrh_6.js";const a={class:"error404"},d=o({__name:"404",setup(c){return(n,_)=>(r(),t("div",a,[s(e,{code:"404",title:"哎呀,出错了!您访问的页面不存在…"})]))}});export{d as default};
|
||||
@@ -1 +0,0 @@
|
||||
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 +0,0 @@
|
||||
function a(e){"@babel/helpers - typeof";return a=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},a(e)}function c(e,t,r,n,f,y,i){try{var u=e[y](i),o=u.value}catch(l){return void r(l)}u.done?t(o):Promise.resolve(o).then(n,f)}function p(e){return function(){var t=this,r=arguments;return new Promise(function(n,f){var y=e.apply(t,r);function i(o){c(y,n,f,i,u,"next",o)}function u(o){c(y,n,f,i,u,"throw",o)}i(void 0)})}}function b(e,t){if(a(e)!="object"||!e)return e;var r=e[Symbol.toPrimitive];if(r!==void 0){var n=r.call(e,t);if(a(n)!="object")return n;throw new TypeError("@@toPrimitive must return a primitive value.")}return(t==="string"?String:Number)(e)}function m(e){var t=b(e,"string");return a(t)=="symbol"?t:t+""}function s(e,t,r){return(t=m(t))in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}export{a as _,p as a,s as b};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
import{H as u}from"../highlight.js-Bxt7hFFy.js";import{f as c,i as g,w as s,A as n}from"../@vue/runtime-core-C6bnekPw.js";import{n as h}from"../@vue/reactivity-DiY1c2vO.js";var i=c({props:{code:{type:String,required:!0},language:{type:String,default:""},autodetect:{type:Boolean,default:!0},ignoreIllegals:{type:Boolean,default:!0}},setup:function(e){var t=h(e.language);s((function(){return e.language}),(function(a){t.value=a}));var r=n((function(){return e.autodetect||!t.value})),o=n((function(){return!r.value&&!u.getLanguage(t.value)}));return{className:n((function(){return o.value?"":"hljs "+t.value})),highlightedCode:n((function(){var a;if(o.value)return console.warn('The language "'+t.value+'" you specified could not be found.'),e.code.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'");if(r.value){var l=u.highlightAuto(e.code);return t.value=(a=l.language)!==null&&a!==void 0?a:"",l.value}return(l=u.highlight(e.code,{language:t.value,ignoreIllegals:e.ignoreIllegals})).value}))}},render:function(){return g("pre",{},[g("code",{class:this.className,innerHTML:this.highlightedCode})])}}),v={install:function(e){e.component("highlightjs",i)},component:i};export{v as o};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
import"./uikit-base-component-vue3-YgTqL4da.js";import{A as r}from"../tuikit-atomicx-vue3-Dln8Zi6e.js";import{f as s,ak as c,I as l,aq as m,G as u,at as i,H as p,A as d}from"../@vue/runtime-core-C6bnekPw.js";import{y as v}from"../@vue/reactivity-DiY1c2vO.js";import"./chat-uikit-engine-zx802ozq.js";const _=(t,o)=>{const a=t.__vccOpts||t;for(const[e,n]of o)a[e]=n;return a},f={key:0,class:"chat"},h=s({name:"Chat",__name:"Chat",props:{PlaceholderEmpty:{default:null}},setup(t){const{activeConversation:o}=r(),a=d(()=>{var e;return!((e=o.value)!=null&&e.conversationID)});return(e,n)=>v(o)?(c(),l("div",f,[m(e.$slots,"default",{},void 0,!0)])):a.value&&t.PlaceholderEmpty?(c(),u(i(t.PlaceholderEmpty),{key:1})):p("",!0)}}),A=_(h,[["__scopeId","data-v-1c9c77cd"]]);typeof window<"u"&&(window.__CHAT_ATOMICX_VUE3__={name:"@tencentcloud/chat-uikit-vue3",version:"4.5.4"},console.log("[@tencentcloud/chat-uikit-vue3] v4.5.4"));export{A as E};
|
||||
@@ -1 +0,0 @@
|
||||
.chat[data-v-1c9c77cd]{display:flex;flex-direction:column;min-width:0}.uikit-chat-header[data-v-a0c42ddc]{padding:14px 10px;height:64px;display:flex;justify-content:center;background-color:var(--bg-color-operate)}.uikit-chat-header__container[data-v-a0c42ddc]{padding:0 10px;flex-direction:row;align-items:center;justify-content:space-between}.uikit-chat-header__left[data-v-a0c42ddc]{flex:1 1 auto;display:flex;flex-direction:row;align-items:center}.uikit-chat-header__avatar[data-v-a0c42ddc]{margin-right:12px}.uikit-chat-header__info[data-v-a0c42ddc]{flex:1;display:flex;flex-direction:column;justify-content:center}.uikit-chat-header__title[data-v-a0c42ddc]{display:block;margin:0;font-size:16px;font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-color-primary)}.uikit-chat-header__typing-indicator[data-v-a0c42ddc]{font-size:12px;color:var(--text-color-secondary)}.uikit-chat-header__live[data-v-a0c42ddc]{margin-top:4px;font-size:12px;color:var(--text-color-secondary)}/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}:root{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}*,*:after,*:before{box-sizing:border-box}ul,li{list-style:none;padding:0;margin:0}picture,img,video,canvas,svg{display:block;max-width:100%}img{max-width:100%;height:auto;vertical-align:middle;image-rendering:-webkit-optimize-contrast;aspect-ratio:attr(width)/attr(height);display:inline-block;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}img:not([src],[srcset]){visibility:hidden}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user