Merge branch 'master' into gancao
This commit is contained in:
@@ -0,0 +1,259 @@
|
|||||||
|
# Diagnosis Image URL Prefix Fix (诊断图片URL前缀修复)
|
||||||
|
|
||||||
|
## Issue
|
||||||
|
Image URLs in the diagnosis detail (tongue_images and report_files) were stored as relative paths without the domain prefix, causing issues when accessing them from external sources or different domains.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
Added logic to automatically prepend the current domain to image URLs that don't already have an `http://` or `https://` prefix.
|
||||||
|
|
||||||
|
## Changes Made
|
||||||
|
|
||||||
|
### File: `server/app/adminapi/logic/tcm/DiagnosisLogic.php`
|
||||||
|
|
||||||
|
#### 1. Updated tongue_images Processing
|
||||||
|
Added domain prefix logic after parsing the images array:
|
||||||
|
|
||||||
|
```php
|
||||||
|
if (!empty($diagnosis['tongue_images'])) {
|
||||||
|
// 先尝试 JSON 解析(兼容旧数据)
|
||||||
|
$files = json_decode($diagnosis['tongue_images'], true);
|
||||||
|
if (is_array($files)) {
|
||||||
|
$diagnosis['tongue_images'] = $files;
|
||||||
|
} else {
|
||||||
|
// JSON 解析失败则按逗号分割
|
||||||
|
$diagnosis['tongue_images'] = array_filter(explode(',', $diagnosis['tongue_images'])) ?: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为没有 http 前缀的图片添加域名
|
||||||
|
$domain = request()->domain();
|
||||||
|
$diagnosis['tongue_images'] = array_map(function($url) use ($domain) {
|
||||||
|
if (empty($url)) {
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
// 如果已经包含 http:// 或 https://,则跳过
|
||||||
|
if (stripos($url, 'http://') === 0 || stripos($url, 'https://') === 0) {
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
// 添加域名前缀
|
||||||
|
return $domain . $url;
|
||||||
|
}, $diagnosis['tongue_images']);
|
||||||
|
} else {
|
||||||
|
$diagnosis['tongue_images'] = [];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Updated report_files Processing
|
||||||
|
Applied the same logic to report files:
|
||||||
|
|
||||||
|
```php
|
||||||
|
if (!empty($diagnosis['report_files'])) {
|
||||||
|
// 先尝试 JSON 解析(兼容旧数据)
|
||||||
|
$files = json_decode($diagnosis['report_files'], true);
|
||||||
|
if (is_array($files)) {
|
||||||
|
$diagnosis['report_files'] = $files;
|
||||||
|
} else {
|
||||||
|
// JSON 解析失败则按逗号分割
|
||||||
|
$diagnosis['report_files'] = array_filter(explode(',', $diagnosis['report_files'])) ?: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为没有 http 前缀的文件添加域名
|
||||||
|
$domain = request()->domain();
|
||||||
|
$diagnosis['report_files'] = array_map(function($url) use ($domain) {
|
||||||
|
if (empty($url)) {
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
// 如果已经包含 http:// 或 https://,则跳过
|
||||||
|
if (stripos($url, 'http://') === 0 || stripos($url, 'https://') === 0) {
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
// 添加域名前缀
|
||||||
|
return $domain . $url;
|
||||||
|
}, $diagnosis['report_files']);
|
||||||
|
} else {
|
||||||
|
$diagnosis['report_files'] = [];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Logic Flow
|
||||||
|
|
||||||
|
### URL Processing Logic:
|
||||||
|
1. Parse the images/files array (JSON or comma-separated)
|
||||||
|
2. Get current domain using `request()->domain()`
|
||||||
|
3. For each URL in the array:
|
||||||
|
- Check if URL is empty → skip
|
||||||
|
- Check if URL starts with `http://` or `https://` → skip (already has protocol)
|
||||||
|
- Otherwise → prepend domain to the URL
|
||||||
|
|
||||||
|
### Examples:
|
||||||
|
|
||||||
|
#### Relative Path (Needs Domain):
|
||||||
|
```php
|
||||||
|
Input: "/uploads/images/tongue/2024/01/image.jpg"
|
||||||
|
Domain: "https://admin.zhenyangtang.com.cn"
|
||||||
|
Output: "https://admin.zhenyangtang.com.cn/uploads/images/tongue/2024/01/image.jpg"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Absolute URL (Skip):
|
||||||
|
```php
|
||||||
|
Input: "https://cdn.example.com/images/tongue.jpg"
|
||||||
|
Output: "https://cdn.example.com/images/tongue.jpg"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### HTTP URL (Skip):
|
||||||
|
```php
|
||||||
|
Input: "http://example.com/image.jpg"
|
||||||
|
Output: "http://example.com/image.jpg"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Empty URL (Skip):
|
||||||
|
```php
|
||||||
|
Input: ""
|
||||||
|
Output: ""
|
||||||
|
```
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
### 1. Cross-Domain Compatibility
|
||||||
|
Images can now be accessed from different domains or external applications without path issues.
|
||||||
|
|
||||||
|
### 2. API Response Consistency
|
||||||
|
API responses always return complete, accessible URLs regardless of how they were stored.
|
||||||
|
|
||||||
|
### 3. Backward Compatibility
|
||||||
|
- Existing relative paths are automatically converted
|
||||||
|
- Existing absolute URLs are preserved
|
||||||
|
- No database migration required
|
||||||
|
|
||||||
|
### 4. Flexible Storage
|
||||||
|
- Database can store either relative or absolute paths
|
||||||
|
- System handles both formats transparently
|
||||||
|
- Future-proof for CDN integration
|
||||||
|
|
||||||
|
## Use Cases
|
||||||
|
|
||||||
|
### 1. Mobile App Access
|
||||||
|
Mobile apps can directly use the returned URLs without needing to construct them.
|
||||||
|
|
||||||
|
### 2. Third-Party Integration
|
||||||
|
External systems can access images using the complete URLs from API responses.
|
||||||
|
|
||||||
|
### 3. CDN Migration
|
||||||
|
When migrating to CDN, images with absolute CDN URLs will work alongside local relative paths.
|
||||||
|
|
||||||
|
### 4. Multi-Domain Setup
|
||||||
|
System works correctly across different domains (dev, staging, production).
|
||||||
|
|
||||||
|
## Testing Steps
|
||||||
|
|
||||||
|
### 1. Test Relative Path URLs
|
||||||
|
1. Create a diagnosis with tongue images stored as relative paths:
|
||||||
|
```
|
||||||
|
/uploads/images/tongue/2024/01/image.jpg
|
||||||
|
```
|
||||||
|
2. Fetch diagnosis detail via API
|
||||||
|
3. Verify response contains:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tongue_images": [
|
||||||
|
"https://admin.zhenyangtang.com.cn/uploads/images/tongue/2024/01/image.jpg"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Test Absolute URLs
|
||||||
|
1. Create a diagnosis with absolute URL:
|
||||||
|
```
|
||||||
|
https://cdn.example.com/images/tongue.jpg
|
||||||
|
```
|
||||||
|
2. Fetch diagnosis detail
|
||||||
|
3. Verify URL is unchanged:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tongue_images": [
|
||||||
|
"https://cdn.example.com/images/tongue.jpg"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Test Mixed URLs
|
||||||
|
1. Create a diagnosis with both relative and absolute URLs:
|
||||||
|
```
|
||||||
|
[
|
||||||
|
"/uploads/local.jpg",
|
||||||
|
"https://cdn.example.com/remote.jpg"
|
||||||
|
]
|
||||||
|
```
|
||||||
|
2. Fetch diagnosis detail
|
||||||
|
3. Verify correct processing:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tongue_images": [
|
||||||
|
"https://admin.zhenyangtang.com.cn/uploads/local.jpg",
|
||||||
|
"https://cdn.example.com/remote.jpg"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Test Empty/Null Values
|
||||||
|
1. Create a diagnosis with empty images
|
||||||
|
2. Fetch diagnosis detail
|
||||||
|
3. Verify empty array is returned:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tongue_images": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Test Report Files
|
||||||
|
Repeat all tests above for `report_files` field.
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
|
||||||
|
### Domain Detection
|
||||||
|
Uses `request()->domain()` which returns the current request domain including protocol:
|
||||||
|
- Development: `http://localhost:8000`
|
||||||
|
- Production: `https://admin.zhenyangtang.com.cn`
|
||||||
|
|
||||||
|
### Case-Insensitive Protocol Check
|
||||||
|
Uses `stripos()` for case-insensitive checking:
|
||||||
|
- Matches: `http://`, `HTTP://`, `Http://`
|
||||||
|
- Matches: `https://`, `HTTPS://`, `Https://`
|
||||||
|
|
||||||
|
### Array Processing
|
||||||
|
Uses `array_map()` for efficient processing of all URLs in the array.
|
||||||
|
|
||||||
|
## Related Files
|
||||||
|
|
||||||
|
### Backend:
|
||||||
|
- `server/app/adminapi/logic/tcm/DiagnosisLogic.php`
|
||||||
|
- Updated `tongue_images` processing
|
||||||
|
- Updated `report_files` processing
|
||||||
|
|
||||||
|
### Database:
|
||||||
|
- `tcm_diagnosis` table
|
||||||
|
- `tongue_images` column (stores JSON or comma-separated paths)
|
||||||
|
- `report_files` column (stores JSON or comma-separated paths)
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
### Storage Format
|
||||||
|
The database continues to store paths as-is (relative or absolute). The conversion happens only when reading data for API responses.
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
The `array_map()` operation is efficient and adds minimal overhead. For typical diagnosis records with 1-5 images, the performance impact is negligible.
|
||||||
|
|
||||||
|
### Future Enhancements
|
||||||
|
If needed, this logic could be extracted into a helper function for reuse across other file/image fields in the system.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
✅ Automatically adds domain prefix to relative image URLs
|
||||||
|
✅ Preserves absolute URLs (http/https)
|
||||||
|
✅ Handles both tongue_images and report_files
|
||||||
|
✅ Backward compatible with existing data
|
||||||
|
✅ Case-insensitive protocol detection
|
||||||
|
✅ Handles empty/null values gracefully
|
||||||
|
✅ No database migration required
|
||||||
|
✅ Works across different domains
|
||||||
|
|
||||||
|
Image URLs in diagnosis details are now always complete and accessible, improving API usability and cross-domain compatibility!
|
||||||
@@ -82,3 +82,15 @@ export function completeAppointment(params: any) {
|
|||||||
export function getDoctorStatistics(params: any) {
|
export function getDoctorStatistics(params: any) {
|
||||||
return request.get({ url: '/doctor.statistics/lists', params })
|
return request.get({ url: '/doctor.statistics/lists', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== 部门统计 ==========
|
||||||
|
|
||||||
|
// 获取部门列表
|
||||||
|
export function getDeptList() {
|
||||||
|
return request.get({ url: '/dept.dept/all' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取部门统计
|
||||||
|
export function getDeptStatistics(params: any) {
|
||||||
|
return request.get({ url: '/doctor.statistics/deptLists', params })
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,356 @@
|
|||||||
|
<template>
|
||||||
|
<div class="dept-statistics-page">
|
||||||
|
<!-- 筛选区域卡片 -->
|
||||||
|
<el-card class="mb-4" shadow="hover">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="card-title">筛选条件</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-form :inline="true" :model="queryParams" class="filter-form">
|
||||||
|
<el-form-item label="部门">
|
||||||
|
<el-select
|
||||||
|
v-model="queryParams.dept_id"
|
||||||
|
placeholder="全部部门"
|
||||||
|
clearable
|
||||||
|
filterable
|
||||||
|
style="width: 200px"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="dept in deptList"
|
||||||
|
:key="dept.id"
|
||||||
|
:label="dept.name"
|
||||||
|
:value="dept.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="时间范围">
|
||||||
|
<el-radio-group v-model="queryParams.time_type" @change="handleTimeTypeChange">
|
||||||
|
<el-radio-button label="today">今天</el-radio-button>
|
||||||
|
<el-radio-button label="week">最近7天</el-radio-button>
|
||||||
|
<el-radio-button label="month">最近30天</el-radio-button>
|
||||||
|
<el-radio-button label="custom">自定义</el-radio-button>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item v-if="queryParams.time_type === 'custom'">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="dateRange"
|
||||||
|
type="daterange"
|
||||||
|
range-separator="至"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
style="width: 260px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="getStatistics" :loading="loading">查询</el-button>
|
||||||
|
<el-button @click="handleReset">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 统计数据卡片 -->
|
||||||
|
<el-card v-loading="loading" class="mb-4" shadow="hover">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="card-title">部门统计数据</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-if="statisticsList.length > 0">
|
||||||
|
<el-table
|
||||||
|
:data="statisticsList"
|
||||||
|
style="width: 100%"
|
||||||
|
:header-cell-style="{ background: '#f5f7fa', color: '#606266', fontWeight: '600' }"
|
||||||
|
>
|
||||||
|
<el-table-column prop="dept_name" label="部门名称" width="120" />
|
||||||
|
|
||||||
|
<!-- 状态明细 -->
|
||||||
|
<el-table-column label="状态明细" min-width="700">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="status-overview">
|
||||||
|
<div class="status-tags">
|
||||||
|
<el-tag type="info" size="small" class="compact-tag">
|
||||||
|
已挂号: {{ row.registered_count }}
|
||||||
|
</el-tag>
|
||||||
|
<el-tag type="success" size="small" class="compact-tag">
|
||||||
|
已完成: {{ row.completed_count }}
|
||||||
|
</el-tag>
|
||||||
|
<el-tag type="danger" size="small" class="compact-tag">
|
||||||
|
已取消: {{ row.canceled_count }}
|
||||||
|
</el-tag>
|
||||||
|
<el-tag type="warning" size="small" class="compact-tag">
|
||||||
|
已过号: {{ row.expired_count }}
|
||||||
|
</el-tag>
|
||||||
|
<el-tag type="primary" size="small" class="compact-tag">
|
||||||
|
总数量: {{ row.total_count }}
|
||||||
|
</el-tag>
|
||||||
|
<el-tag :type="getCompletionRateType(row.completion_rate)" size="small" class="compact-tag">
|
||||||
|
完成率: {{ row.completion_rate }}%
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="time_range" label="统计时间" min-width="200" />
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
<div v-else class="empty-data">
|
||||||
|
<el-empty description="暂无统计数据" />
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { getDeptStatistics, getDeptList } from '@/api/doctor'
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const deptList = ref<any[]>([])
|
||||||
|
const statisticsList = ref<any[]>([])
|
||||||
|
const dateRange = ref<string[]>([])
|
||||||
|
|
||||||
|
const queryParams = reactive({
|
||||||
|
dept_id: '',
|
||||||
|
time_type: 'today',
|
||||||
|
start_date: '',
|
||||||
|
end_date: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const getDeptLists = async () => {
|
||||||
|
try {
|
||||||
|
const res = await getDeptList()
|
||||||
|
// 将树形结构转换为扁平化列表
|
||||||
|
const flattenDepts = (depts) => {
|
||||||
|
let result = []
|
||||||
|
depts.forEach(dept => {
|
||||||
|
result.push({ id: dept.id, name: dept.name })
|
||||||
|
if (dept.children && dept.children.length > 0) {
|
||||||
|
result = result.concat(flattenDepts(dept.children))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
deptList.value = flattenDepts(res) || []
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取部门列表失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatistics = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const params: any = {
|
||||||
|
time_type: queryParams.time_type
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queryParams.dept_id) {
|
||||||
|
params.dept_id = queryParams.dept_id
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queryParams.time_type === 'custom' && dateRange.value && dateRange.value.length === 2) {
|
||||||
|
params.start_date = dateRange.value[0]
|
||||||
|
params.end_date = dateRange.value[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await getDeptStatistics(params)
|
||||||
|
statisticsList.value = res.lists || []
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('获取统计数据错误:', error)
|
||||||
|
ElMessage.error(error.msg || '获取统计数据失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTimeTypeChange = () => {
|
||||||
|
if (queryParams.time_type !== 'custom') {
|
||||||
|
dateRange.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
queryParams.dept_id = ''
|
||||||
|
queryParams.time_type = 'today'
|
||||||
|
dateRange.value = []
|
||||||
|
getStatistics()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取完成率标签类型
|
||||||
|
const getCompletionRateType = (rate: number) => {
|
||||||
|
if (rate >= 80) {
|
||||||
|
return 'success'
|
||||||
|
} else if (rate >= 60) {
|
||||||
|
return 'primary'
|
||||||
|
} else {
|
||||||
|
return 'warning'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getDeptLists()
|
||||||
|
getStatistics()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.dept-statistics-page {
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-form {
|
||||||
|
padding: 10px 0;
|
||||||
|
:deep(.el-form-item) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-overview {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-section {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
border-bottom: 1px solid #ebeef5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-tag {
|
||||||
|
font-size: 12px !important;
|
||||||
|
padding: 4px 12px !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1) !important;
|
||||||
|
transition: all 0.3s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-tag:hover {
|
||||||
|
transform: translateY(-1px) !important;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-data {
|
||||||
|
padding: 40px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格样式优化 */
|
||||||
|
:deep(.el-table) {
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||||
|
transition: box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table:hover) {
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table th) {
|
||||||
|
background-color: #f5f7fa !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
color: #606266 !important;
|
||||||
|
border-bottom: 1px solid #ebeef5 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table tr:hover) {
|
||||||
|
background-color: #f5f7fa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__cell) {
|
||||||
|
text-align: left !important;
|
||||||
|
vertical-align: top !important;
|
||||||
|
padding: 12px 15px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片样式优化 */
|
||||||
|
:deep(.el-card) {
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1) !important;
|
||||||
|
margin-bottom: 16px !important;
|
||||||
|
transition: box-shadow 0.3s ease;
|
||||||
|
border: none !important;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-card:hover) {
|
||||||
|
box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.15) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-card__header) {
|
||||||
|
background-color: #ffffff !important;
|
||||||
|
border-bottom: 1px solid #ebeef5 !important;
|
||||||
|
padding: 15px 20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-card__body) {
|
||||||
|
padding: 20px !important;
|
||||||
|
background-color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮样式优化 */
|
||||||
|
:deep(.el-button) {
|
||||||
|
border-radius: 4px !important;
|
||||||
|
transition: all 0.3s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-button:hover) {
|
||||||
|
transform: translateY(-1px) !important;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 选择器样式优化 */
|
||||||
|
:deep(.el-select) {
|
||||||
|
border-radius: 4px !important;
|
||||||
|
transition: all 0.3s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-select:hover) {
|
||||||
|
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 标签样式优化 */
|
||||||
|
:deep(.el-tag) {
|
||||||
|
border-radius: 4px !important;
|
||||||
|
transition: all 0.3s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-tag:hover) {
|
||||||
|
transform: translateY(-1px) !important;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15) !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -20,4 +20,13 @@ class StatisticsController extends BaseAdminController
|
|||||||
{
|
{
|
||||||
return $this->dataLists(new StatisticsLists());
|
return $this->dataLists(new StatisticsLists());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notes 部门统计列表
|
||||||
|
* @return \think\response\Json
|
||||||
|
*/
|
||||||
|
public function deptLists()
|
||||||
|
{
|
||||||
|
return $this->dataLists(new StatisticsLists('dept'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,17 @@ use app\common\model\auth\Admin;
|
|||||||
*/
|
*/
|
||||||
class StatisticsLists extends BaseAdminDataLists
|
class StatisticsLists extends BaseAdminDataLists
|
||||||
{
|
{
|
||||||
|
protected $type = 'doctor';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $type 统计类型:doctor(医生统计) 或 dept(部门统计)
|
||||||
|
*/
|
||||||
|
public function __construct($type = 'doctor')
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
$this->type = $type;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @notes 搜索条件
|
* @notes 搜索条件
|
||||||
* @return array
|
* @return array
|
||||||
@@ -28,6 +39,10 @@ class StatisticsLists extends BaseAdminDataLists
|
|||||||
*/
|
*/
|
||||||
public function lists(): array
|
public function lists(): array
|
||||||
{
|
{
|
||||||
|
if ($this->type === 'dept') {
|
||||||
|
return $this->getDeptStatistics();
|
||||||
|
}
|
||||||
|
|
||||||
$doctorId = $this->params['doctor_id'] ?? '';
|
$doctorId = $this->params['doctor_id'] ?? '';
|
||||||
$timeType = $this->params['time_type'] ?? 'today';
|
$timeType = $this->params['time_type'] ?? 'today';
|
||||||
$startDate = $this->params['start_date'] ?? '';
|
$startDate = $this->params['start_date'] ?? '';
|
||||||
@@ -377,6 +392,68 @@ class StatisticsLists extends BaseAdminDataLists
|
|||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notes 获取部门统计数据
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function getDeptStatistics()
|
||||||
|
{
|
||||||
|
$deptId = $this->params['dept_id'] ?? '';
|
||||||
|
$timeType = $this->params['time_type'] ?? 'today';
|
||||||
|
$startDate = $this->params['start_date'] ?? '';
|
||||||
|
$endDate = $this->params['end_date'] ?? '';
|
||||||
|
|
||||||
|
// 计算时间范围
|
||||||
|
list($startDate, $endDate, $timeRangeText) = $this->getTimeRange($timeType, $startDate, $endDate);
|
||||||
|
|
||||||
|
// 查询部门统计数据
|
||||||
|
$deptStatsQuery = \think\facade\Db::name('doctor_appointment')
|
||||||
|
->alias('apt')
|
||||||
|
->leftJoin('zyt_admin_dept ad', 'apt.assistant_id = ad.admin_id')
|
||||||
|
->leftJoin('zyt_dept dept', 'ad.dept_id = dept.id')
|
||||||
|
->field([
|
||||||
|
'dept.id as dept_id',
|
||||||
|
'dept.name as dept_name',
|
||||||
|
'COUNT(*) as total_count',
|
||||||
|
'SUM(CASE WHEN apt.status = 1 THEN 1 ELSE 0 END) as registered_count',
|
||||||
|
'SUM(CASE WHEN apt.status = 3 THEN 1 ELSE 0 END) as completed_count',
|
||||||
|
'SUM(CASE WHEN apt.status = 2 THEN 1 ELSE 0 END) as canceled_count',
|
||||||
|
'SUM(CASE WHEN apt.status = 4 THEN 1 ELSE 0 END) as expired_count'
|
||||||
|
])
|
||||||
|
->where('apt.appointment_date', '>=', $startDate)
|
||||||
|
->where('apt.appointment_date', '<=', $endDate)
|
||||||
|
->whereNotNull('dept.id');
|
||||||
|
|
||||||
|
if ($deptId) {
|
||||||
|
$deptStatsQuery->where('dept.id', $deptId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$deptStats = $deptStatsQuery->group('dept.id')->select()->toArray();
|
||||||
|
|
||||||
|
// 组装结果
|
||||||
|
$result = [];
|
||||||
|
foreach ($deptStats as $stat) {
|
||||||
|
// 计算完成率
|
||||||
|
$completionRate = $stat['total_count'] > 0
|
||||||
|
? round(($stat['completed_count'] / $stat['total_count']) * 100, 2)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
$result[] = [
|
||||||
|
'dept_id' => $stat['dept_id'],
|
||||||
|
'dept_name' => $stat['dept_name'],
|
||||||
|
'total_count' => (int)$stat['total_count'] ?? 0,
|
||||||
|
'registered_count' => (int)$stat['registered_count'] ?? 0,
|
||||||
|
'completed_count' => (int)$stat['completed_count'] ?? 0,
|
||||||
|
'canceled_count' => (int)$stat['canceled_count'] ?? 0,
|
||||||
|
'expired_count' => (int)$stat['expired_count'] ?? 0,
|
||||||
|
'completion_rate' => $completionRate,
|
||||||
|
'time_range' => $timeRangeText
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @notes 获取时间范围
|
* @notes 获取时间范围
|
||||||
* @param string $timeType
|
* @param string $timeType
|
||||||
|
|||||||
@@ -255,6 +255,20 @@ class DiagnosisLogic extends BaseLogic
|
|||||||
// JSON 解析失败则按逗号分割
|
// JSON 解析失败则按逗号分割
|
||||||
$diagnosis['tongue_images'] = array_filter(explode(',', $diagnosis['tongue_images'])) ?: [];
|
$diagnosis['tongue_images'] = array_filter(explode(',', $diagnosis['tongue_images'])) ?: [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 为没有 http 前缀的图片添加域名
|
||||||
|
$domain = request()->domain();
|
||||||
|
$diagnosis['tongue_images'] = array_map(function($url) use ($domain) {
|
||||||
|
if (empty($url)) {
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
// 如果已经包含 http:// 或 https://,则跳过
|
||||||
|
if (stripos($url, 'http://') === 0 || stripos($url, 'https://') === 0) {
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
// 添加域名前缀
|
||||||
|
return $domain .'/'. $url;
|
||||||
|
}, $diagnosis['tongue_images']);
|
||||||
} else {
|
} else {
|
||||||
$diagnosis['tongue_images'] = [];
|
$diagnosis['tongue_images'] = [];
|
||||||
}
|
}
|
||||||
@@ -269,6 +283,20 @@ class DiagnosisLogic extends BaseLogic
|
|||||||
// JSON 解析失败则按逗号分割
|
// JSON 解析失败则按逗号分割
|
||||||
$diagnosis['report_files'] = array_filter(explode(',', $diagnosis['report_files'])) ?: [];
|
$diagnosis['report_files'] = array_filter(explode(',', $diagnosis['report_files'])) ?: [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 为没有 http 前缀的文件添加域名
|
||||||
|
$domain = request()->domain();
|
||||||
|
$diagnosis['report_files'] = array_map(function($url) use ($domain) {
|
||||||
|
if (empty($url)) {
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
// 如果已经包含 http:// 或 https://,则跳过
|
||||||
|
if (stripos($url, 'http://') === 0 || stripos($url, 'https://') === 0) {
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
// 添加域名前缀
|
||||||
|
return $domain . $url;
|
||||||
|
}, $diagnosis['report_files']);
|
||||||
} else {
|
} else {
|
||||||
$diagnosis['report_files'] = [];
|
$diagnosis['report_files'] = [];
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user