From 8022229e717f9f7e4c17cf72c239aa0a751c164b Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 16 Apr 2026 11:13:26 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ORDER_DETAIL_ADDRESS_DISPLAY_FIX.md | 167 +++++++++ ORDER_LIST_REGION_UPDATE.md | 191 +++++++++++ REGION_DATABASE_SAVE_FIX.md | 170 ++++++++++ REGION_DATA_SETUP.md | 72 ++++ REGION_FIELD_SUBMISSION_FIX.md | 153 +++++++++ REGION_SELECTOR_AND_DOSAGE_FIX.md | 199 +++++++++++ SERVICE_PACKAGE_MULTI_SELECT.md | 293 ++++++++++++++++ add_bags_per_dose_field.sql | 3 + .../src/components/tcm-prescription/index.vue | 40 ++- .../src/views/consumer/prescription/index.vue | 321 +++++++++++------- .../consumer/prescription/order_list.vue | 247 +++++++++----- admin/vite.config.ts | 9 +- .../adminapi/logic/tcm/PrescriptionLogic.php | 2 + .../logic/tcm/PrescriptionOrderLogic.php | 12 + .../validate/tcm/PrescriptionValidate.php | 10 +- .../gancao/GancaoScmRecipelService.php | 45 ++- 16 files changed, 1703 insertions(+), 231 deletions(-) create mode 100644 ORDER_DETAIL_ADDRESS_DISPLAY_FIX.md create mode 100644 ORDER_LIST_REGION_UPDATE.md create mode 100644 REGION_DATABASE_SAVE_FIX.md create mode 100644 REGION_DATA_SETUP.md create mode 100644 REGION_FIELD_SUBMISSION_FIX.md create mode 100644 REGION_SELECTOR_AND_DOSAGE_FIX.md create mode 100644 SERVICE_PACKAGE_MULTI_SELECT.md create mode 100644 add_bags_per_dose_field.sql diff --git a/ORDER_DETAIL_ADDRESS_DISPLAY_FIX.md b/ORDER_DETAIL_ADDRESS_DISPLAY_FIX.md new file mode 100644 index 00000000..b62c76d8 --- /dev/null +++ b/ORDER_DETAIL_ADDRESS_DISPLAY_FIX.md @@ -0,0 +1,167 @@ +# Order Detail Address Display Fix (订单详情地址显示修复) + +## Issue +The order detail view was only showing the detailed address (`shipping_address`) without displaying the province, city, and district information. + +## Changes Made + +### 1. Added Computed Property for Full Address +**File**: `admin/src/views/consumer/prescription/order_list.vue` + +Added `detailFullAddress` computed property that combines all address components: + +```typescript +// 完整收货地址(省市区 + 详细地址) +const detailFullAddress = computed(() => { + const d = detailData.value + if (!d) return '—' + + const parts = [] + if (d.shipping_province) parts.push(d.shipping_province) + if (d.shipping_city) parts.push(d.shipping_city) + if (d.shipping_district) parts.push(d.shipping_district) + if (d.shipping_address) parts.push(d.shipping_address) + + return parts.length > 0 ? parts.join(' ') : '—' +}) +``` + +### 2. Updated Display Template +Changed the shipping address display from: +```vue + + {{ detailData.shipping_address }} + +``` + +To: +```vue + + {{ detailFullAddress }} + +``` + +## Display Format + +### Before: +``` +收货地址: 顶顶顶 +``` + +### After: +``` +收货地址: 北京市 北京市 丰台区 顶顶顶 +``` + +## Logic + +The `detailFullAddress` computed property: +1. Checks if `detailData` exists +2. Collects non-empty address components in order: + - `shipping_province` (省) + - `shipping_city` (市) + - `shipping_district` (区/县) + - `shipping_address` (详细地址) +3. Joins them with spaces +4. Returns '—' if no address components exist + +## Examples + +### Complete Address: +``` +Input: + shipping_province: "四川省" + shipping_city: "成都市" + shipping_district: "武侯区" + shipping_address: "天府大道中段666号" + +Output: + 四川省 成都市 武侯区 天府大道中段666号 +``` + +### Partial Address (Missing Province): +``` +Input: + shipping_province: null + shipping_city: "成都市" + shipping_district: "武侯区" + shipping_address: "天府大道中段666号" + +Output: + 成都市 武侯区 天府大道中段666号 +``` + +### Only Detailed Address: +``` +Input: + shipping_province: null + shipping_city: null + shipping_district: null + shipping_address: "天府大道中段666号" + +Output: + 天府大道中段666号 +``` + +### No Address: +``` +Input: + shipping_province: null + shipping_city: null + shipping_district: null + shipping_address: null + +Output: + — +``` + +## Testing Steps + +### 1. Test Complete Address Display +1. Open an order that has province/city/district data +2. Check the "收货地址" field in the detail view +3. Verify it shows: `省 市 区 详细地址` + +### 2. Test Legacy Orders (No Region Data) +1. Open an old order that only has `shipping_address` +2. Verify it still displays the detailed address correctly +3. Should show just the detailed address without extra spaces + +### 3. Test Empty Address +1. Create an order without any address information +2. Verify it displays "—" instead of empty string + +### 4. Test Different Address Combinations +Test various combinations: +- Full address (province + city + district + detail) +- City + district + detail (no province) +- District + detail (no province/city) +- Detail only (no region data) +- Empty (no data at all) + +## Related Files + +### Frontend: +- `admin/src/views/consumer/prescription/order_list.vue` + - Added `detailFullAddress` computed property + - Updated shipping address display template + +### Backend: +- `server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php` + - Already returns `shipping_province`, `shipping_city`, `shipping_district` in detail response + +### Database: +- `tcm_prescription_order` table + - Columns: `shipping_province`, `shipping_city`, `shipping_district`, `shipping_address` + +## Benefits + +✅ **Complete Information**: Users can now see the full address including province/city/district +✅ **Better Readability**: Address components are clearly separated with spaces +✅ **Backward Compatible**: Works with legacy orders that only have detailed address +✅ **Graceful Handling**: Shows "—" when no address data exists +✅ **Flexible**: Handles partial address data (missing province, city, or district) + +## Summary + +The order detail view now displays the complete shipping address including province, city, district, and detailed address, providing users with full address information at a glance! diff --git a/ORDER_LIST_REGION_UPDATE.md b/ORDER_LIST_REGION_UPDATE.md new file mode 100644 index 00000000..c82f3536 --- /dev/null +++ b/ORDER_LIST_REGION_UPDATE.md @@ -0,0 +1,191 @@ +# Order List Region Selector Update (订单列表省市区选择器更新) + +## Changes Made + +Updated the order list view (`order_list.vue`) to use the same API-based region data loading as the prescription index view. + +### 1. Replaced Static Region Data with API Loading +**File**: `admin/src/views/consumer/prescription/order_list.vue` + +#### Before: +- Static hardcoded region data with limited provinces/cities +- Only included: 四川省, 北京市, 上海市, 广东省 + +#### After: +- Dynamic API-based region data loading +- Fetches complete China region data from `/api-proxy/area/ChinaCitys.json` +- Includes all provinces, cities, and districts +- Falls back to basic data if API fails + +### 2. Added Data Transformation Function +Added `transformRegionData()` function to convert API format to el-cascader format: +- API format: `{province, citys: [{city, areas: [{area}]}]}` +- Cascader format: `{value, label, children: [{value, label, children: [{value, label}]}]}` + +### 3. Updated Cascader Component +Added missing props to the cascader: +- `emitPath: true` - Returns full path array instead of just last value +- `filterable` - Enables search functionality + +### 4. Updated onMounted Hook +Changed from: +```typescript +onMounted(() => { + getLists() +}) +``` + +To: +```typescript +onMounted(async () => { + await loadRegionData() + getLists() +}) +``` + +### 5. Added Region Fields to Quick Edit +Updated the quick tracking number edit function to include region fields: +```typescript +shipping_province: d.shipping_province || '', +shipping_city: d.shipping_city || '', +shipping_district: d.shipping_district || '', +``` + +This ensures region data is preserved when only updating tracking numbers. + +## Data Flow + +### Loading Region Data: +1. Component mounts +2. `loadRegionData()` fetches from `/api-proxy/area/ChinaCitys.json` +3. `transformRegionData()` converts API format to cascader format +4. `regionOptions` is populated with all China regions +5. If API fails, falls back to basic region data + +### Editing Order with Region: +1. User opens edit dialog +2. Region data is loaded from order: `[province, city, district]` +3. Cascader displays the selected region +4. User can change region selection +5. On save, region array is split into three fields: + - `shipping_province` + - `shipping_city` + - `shipping_district` +6. Backend saves the three separate fields + +### Quick Edit (Tracking Number): +1. User clicks quick edit for tracking number +2. System fetches full order details +3. Includes region fields in the update payload +4. Region data is preserved while updating tracking number + +## Code Structure + +### Region Data Loading: +```typescript +const regionOptions = ref([]) + +const transformRegionData = (data: any[]) => { + // Converts API format to cascader format +} + +const loadRegionData = async () => { + // Fetches from API and transforms data + // Falls back to basic data on error +} +``` + +### Region Field Handling in Edit: +```typescript +// Loading data into form: +const province = d.shipping_province || '' +const city = d.shipping_city || '' +const district = d.shipping_district || '' +if (province || city || district) { + editForm.region = [province, city, district].filter(v => v !== '') +} + +// Saving data from form: +if (editForm.region && editForm.region.length > 0) { + payload.shipping_province = editForm.region[0] || '' + payload.shipping_city = editForm.region[1] || '' + payload.shipping_district = editForm.region[2] || '' +} +``` + +## Testing Steps + +### 1. Test Region Data Loading +1. Open order list page +2. Check browser console for: + ``` + 开始加载省市区数据... + 响应状态: 200 + 加载的数据条数: XX + regionOptions 已设置,条数: XX + ``` +3. If you see errors, check that dev server was restarted + +### 2. Test Edit Order with Region +1. Click "编辑" on any order +2. Check that the region cascader shows the current region (if set) +3. Change the region selection +4. Save the order +5. Verify database is updated: + ```sql + SELECT shipping_province, shipping_city, shipping_district + FROM tcm_prescription_order + WHERE id = XX; + ``` + +### 3. Test Region Search +1. Open edit dialog +2. Click on region cascader +3. Type a province/city name in the search box +4. Verify search results appear +5. Select a region from search results + +### 4. Test Quick Edit Preserves Region +1. Find an order with region data set +2. Click "快递单号" quick edit button +3. Update tracking number +4. Save +5. Verify region data is still present in database + +### 5. Test Fallback Data +1. Stop the dev server +2. Open order list page +3. Verify fallback region data is used (四川省, 北京市) +4. Restart dev server +5. Refresh page +6. Verify full region data is loaded + +## Related Files + +### Frontend: +- `admin/src/views/consumer/prescription/order_list.vue` + - Added `loadRegionData()` and `transformRegionData()` functions + - Updated `onMounted()` to load region data + - Updated cascader component with `emitPath: true` and `filterable` + - Added region fields to quick edit function + - Region field handling already existed in main edit function + +### Backend: +- `server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php` + - Already handles `shipping_province`, `shipping_city`, `shipping_district` in both `create()` and `edit()` methods + +### Configuration: +- `admin/vite.config.ts` + - Proxy configuration for `/api-proxy` already exists + +## Summary + +✅ Replaced static region data with API-based loading +✅ Added data transformation function +✅ Updated cascader with `emitPath: true` and `filterable` +✅ Updated `onMounted()` to load region data +✅ Added region fields to quick edit function +✅ Fallback data available if API fails +✅ Search functionality enabled + +The order list now uses the same comprehensive region data as the prescription index, providing access to all provinces, cities, and districts in China! diff --git a/REGION_DATABASE_SAVE_FIX.md b/REGION_DATABASE_SAVE_FIX.md new file mode 100644 index 00000000..c78e0267 --- /dev/null +++ b/REGION_DATABASE_SAVE_FIX.md @@ -0,0 +1,170 @@ +# Region Database Save Fix (省市区数据库保存修复) + +## Issue +The region fields (province, city, district) were being sent in the API request payload but were not being saved to the database. + +## Root Cause Analysis + +### Frontend Issue +The frontend was sending the wrong parameter names: +- Sent: `province`, `city`, `district` +- Expected by backend: `shipping_province`, `shipping_city`, `shipping_district` + +### Backend Issue +The `create()` method in `PrescriptionOrderLogic.php` was not handling the province/city/district fields at all, even though the `edit()` method had the code to handle them. + +## Changes Made + +### 1. Frontend - Fixed Parameter Names +**File**: `admin/src/views/consumer/prescription/index.vue` + +Changed the payload to use the correct parameter names: + +```typescript +// Before: +payload.province = createOrderForm.region[0] +payload.city = createOrderForm.region[1] +payload.district = createOrderForm.region[2] + +// After: +payload.shipping_province = createOrderForm.region[0] +payload.shipping_city = createOrderForm.region[1] +payload.shipping_district = createOrderForm.region[2] +``` + +### 2. Backend - Added Region Fields to create() Method +**File**: `server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php` + +Added code to handle province/city/district fields in the `create()` method: + +```php +// 处理省市区字段 +if (isset($params['shipping_province'])) { + $order->shipping_province = (string) $params['shipping_province']; +} +if (isset($params['shipping_city'])) { + $order->shipping_city = (string) $params['shipping_city']; +} +if (isset($params['shipping_district'])) { + $order->shipping_district = (string) $params['shipping_district']; +} +``` + +This matches the existing code in the `edit()` method. + +## Data Flow (After Fix) + +### Create Order Flow: +1. User selects region: 北京市 → 北京市 → 丰台区 +2. Frontend stores: `createOrderForm.region = ['北京市', '北京市', '丰台区']` +3. Frontend sends payload: + ```json + { + "shipping_province": "北京市", + "shipping_city": "北京市", + "shipping_district": "丰台区", + ...other fields + } + ``` +4. Backend `create()` method processes: + ```php + $order->shipping_province = "北京市"; + $order->shipping_city = "北京市"; + $order->shipping_district = "丰台区"; + ``` +5. Database columns are populated: + ``` + shipping_province: 北京市 + shipping_city: 北京市 + shipping_district: 丰台区 + ``` + +## Testing Steps + +### 1. Test Create Order with Region +1. Open prescription list page +2. Click "创建订单" on any prescription +3. Fill in required fields: + - Select patient + - Enter recipient name and phone + - **Select region**: 北京市 → 北京市 → 丰台区 + - Enter detailed address +4. Click "创建业务订单" +5. Check browser Network tab: + - Verify payload includes: + ```json + { + "shipping_province": "北京市", + "shipping_city": "北京市", + "shipping_district": "丰台区" + } + ``` + +### 2. Verify Database +After creating the order, check the database: + +```sql +SELECT + id, + order_no, + shipping_province, + shipping_city, + shipping_district, + shipping_address +FROM tcm_prescription_order +ORDER BY id DESC +LIMIT 1; +``` + +Expected result: +``` +| id | order_no | shipping_province | shipping_city | shipping_district | shipping_address | +|----|----------|-------------------|---------------|-------------------|------------------| +| XX | POXXXXXX | 北京市 | 北京市 | 丰台区 | 顶顶顶 | +``` + +### 3. Test Different Regions +Test with various regions to ensure all work correctly: +- 四川省 → 成都市 → 武侯区 +- 上海市 → 上海市 → 浦东新区 +- 广东省 → 深圳市 → 南山区 + +### 4. Test Edit Order +1. Edit an existing order +2. Change the region +3. Save and verify the database is updated + +## Database Schema + +The database should have these columns (from `add_shipping_region_fields.sql`): + +```sql +ALTER TABLE `tcm_prescription_order` +ADD COLUMN `shipping_province` VARCHAR(50) DEFAULT NULL COMMENT '收货省份' AFTER `shipping_address`, +ADD COLUMN `shipping_city` VARCHAR(50) DEFAULT NULL COMMENT '收货城市' AFTER `shipping_province`, +ADD COLUMN `shipping_district` VARCHAR(50) DEFAULT NULL COMMENT '收货区县' AFTER `shipping_city`; +``` + +## Related Files + +### Frontend: +- `admin/src/views/consumer/prescription/index.vue` + - Changed parameter names in `submitCreateOrderFromPrescription()` + +### Backend: +- `server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php` + - Added province/city/district handling in `create()` method + - Already had province/city/district handling in `edit()` method + +### Database: +- `add_shipping_region_fields.sql` (migration file) + +## Summary + +✅ Fixed frontend parameter names: `province` → `shipping_province`, etc. +✅ Added province/city/district handling to backend `create()` method +✅ Backend `edit()` method already had the correct code +✅ Database columns will now be populated correctly on order creation +✅ Database columns will be updated correctly on order edit + +The region fields are now properly saved to the database for both create and edit operations! diff --git a/REGION_DATA_SETUP.md b/REGION_DATA_SETUP.md new file mode 100644 index 00000000..83367146 --- /dev/null +++ b/REGION_DATA_SETUP.md @@ -0,0 +1,72 @@ +# 省市区数据加载说明 + +## 问题 +省市区下拉框显示为空 + +## 原因 +1. Vite代理配置需要重启开发服务器才能生效 +2. 如果代理加载失败,会使用后备数据 + +## 解决步骤 + +### 1. 重启开发服务器 +```bash +# 停止当前运行的开发服务器 (Ctrl+C) +# 然后重新启动 +cd admin +npm run dev +``` + +### 2. 检查控制台日志 +打开浏览器开发者工具的Console标签,应该能看到: +- "开始加载省市区数据..." +- "响应状态: 200" +- "加载的数据条数: XX" +- "regionOptions 已设置,条数: XX" + +### 3. 如果代理失败 +如果看到错误信息,会自动使用后备数据,包含: +- 四川省(成都市、绵阳市) +- 北京市 +- 上海市 +- 广东省(广州市、深圳市) + +## 配置说明 + +### Vite代理配置 (admin/vite.config.ts) +```typescript +server: { + proxy: { + '/api-proxy': { + target: 'https://admin.zhenyangtang.com.cn', + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api-proxy/, '') + } + } +} +``` + +### 前端请求 (admin/src/views/consumer/prescription/index.vue) +```javascript +const response = await fetch('/api-proxy/area/ChinaCitys.json') +``` + +## 生产环境配置 + +生产环境需要在nginx配置代理: + +```nginx +location /api-proxy/ { + proxy_pass https://admin.zhenyangtang.com.cn/; + proxy_set_header Host admin.zhenyangtang.com.cn; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +} +``` + +## 测试 +1. 打开创建订单对话框 +2. 查看"省市区"下拉框 +3. 应该能看到省份列表 +4. 选择省份后能看到城市列表 +5. 选择城市后能看到区县列表 diff --git a/REGION_FIELD_SUBMISSION_FIX.md b/REGION_FIELD_SUBMISSION_FIX.md new file mode 100644 index 00000000..abccae67 --- /dev/null +++ b/REGION_FIELD_SUBMISSION_FIX.md @@ -0,0 +1,153 @@ +# Region Field Submission Fix (省市区字段提交修复) + +## Issue +The region selector (省市区) in the create order form was not submitting the selected province, city, and district values to the database. + +## Root Cause +1. The cascader component had `emitPath: false`, which only returned the last selected value (district name) instead of the full path +2. The `submitCreateOrderFromPrescription()` function was not including the region fields in the payload sent to the backend + +## Changes Made + +### 1. Updated Cascader Configuration +**File**: `admin/src/views/consumer/prescription/index.vue` + +Changed `emitPath` from `false` to `true` to get the full path array: + +```typescript +// Before: +:props="{ value: 'name', label: 'name', children: 'children', emitPath: false, checkStrictly: false }" + +// After: +:props="{ value: 'name', label: 'name', children: 'children', emitPath: true, checkStrictly: false }" +``` + +**Effect**: Now `createOrderForm.region` will contain an array like `['四川省', '成都市', '锦江区']` instead of just `'锦江区'` + +### 2. Updated Form Submission +**File**: `admin/src/views/consumer/prescription/index.vue` + +Added code to extract province, city, and district from the region array and include them in the payload: + +```typescript +// 添加省市区字段 +if (createOrderForm.region && createOrderForm.region.length >= 3) { + payload.province = createOrderForm.region[0] + payload.city = createOrderForm.region[1] + payload.district = createOrderForm.region[2] +} +``` + +### 3. Updated handleRegionChange Comment +Added clarification that the value is now an array of the full path. + +## Data Flow + +### Before Fix: +1. User selects: 四川省 → 成都市 → 锦江区 +2. `createOrderForm.region` = `'锦江区'` (only last value) +3. Payload sent to backend: **No province/city/district fields** +4. Database: province, city, district columns remain empty + +### After Fix: +1. User selects: 四川省 → 成都市 → 锦江区 +2. `createOrderForm.region` = `['四川省', '成都市', '锦江区']` (full path) +3. Payload sent to backend: + ```json + { + "province": "四川省", + "city": "成都市", + "district": "锦江区", + ...other fields + } + ``` +4. Database: province, city, district columns are populated correctly + +## Testing Steps + +### 1. Test Region Selection and Submission +1. Open the prescription list page +2. Click "创建订单" button on any prescription +3. Fill in the required fields: + - Select a patient (患者) + - Enter recipient name (收货人) + - Enter recipient phone (收货手机) + - **Select province/city/district** (省市区): e.g., 四川省 → 成都市 → 锦江区 + - Enter detailed address (详细地址) +4. Click "创建业务订单" button +5. Check browser Network tab: + - Find the API request to create order + - Verify the payload includes: + ```json + { + "province": "四川省", + "city": "成都市", + "district": "锦江区" + } + ``` + +### 2. Verify Database Storage +After creating an order, check the database: + +```sql +SELECT id, order_no, province, city, district, shipping_address +FROM tcm_prescription_order +ORDER BY id DESC +LIMIT 1; +``` + +Expected result: +``` +| id | order_no | province | city | district | shipping_address | +|----|----------|----------|--------|----------|------------------| +| XX | XXXXXX | 四川省 | 成都市 | 锦江区 | 中关村... | +``` + +### 3. Test Different Regions +Test with various regions to ensure the data structure works correctly: +- 北京市 → 北京市 → 朝阳区 +- 上海市 → 上海市 → 浦东新区 +- 广东省 → 深圳市 → 南山区 + +### 4. Test Edge Cases +1. **No region selected**: Should still allow submission (if not required) +2. **Only province selected**: Should not submit incomplete data +3. **Clear selection**: Click the clear button (×) and verify region is cleared + +## Backend Verification + +The backend should already have the database columns from the migration `add_shipping_region_fields.sql`: +- `province` (varchar) +- `city` (varchar) +- `district` (varchar) + +If the backend validation or logic needs to be updated, check: +- `server/app/adminapi/validate/tcm/PrescriptionOrderValidate.php` +- `server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php` + +## Console Debugging + +If you need to debug, check the browser console for the log message: +``` +选择的省市区: ['四川省', '成都市', '锦江区'] +``` + +This confirms the cascader is emitting the correct data structure. + +## Related Files + +### Frontend: +- `admin/src/views/consumer/prescription/index.vue` (main changes) + +### Backend: +- `server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php` (should handle province/city/district) +- `add_shipping_region_fields.sql` (database migration) + +## Summary + +✅ Changed cascader `emitPath` to `true` to get full path array +✅ Added province/city/district extraction in form submission +✅ Payload now includes all three region fields +✅ Database columns will be populated correctly + +The region selector now properly submits province, city, and district values to the database. diff --git a/REGION_SELECTOR_AND_DOSAGE_FIX.md b/REGION_SELECTOR_AND_DOSAGE_FIX.md new file mode 100644 index 00000000..2cb8ca3a --- /dev/null +++ b/REGION_SELECTOR_AND_DOSAGE_FIX.md @@ -0,0 +1,199 @@ +# Region Selector and Dosage Fields Fix + +## Issues Identified + +### 1. Region Selector Empty (省市区下拉为空) +**Problem**: The region cascader dropdown is empty when opening the create order form. + +**Root Cause**: The Vite proxy configuration is correct, but the development server needs to be restarted for the proxy to take effect. + +**Solution**: +- The proxy configuration in `admin/vite.config.ts` is already set up correctly: +```typescript +proxy: { + '/api-proxy': { + target: 'https://admin.zhenyangtang.com.cn', + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api-proxy/, '') + } +} +``` +- The `transformRegionData()` function correctly converts the API format to el-cascader format +- **ACTION REQUIRED**: Restart the development server + +### 2. Dosage Dropdown Not Showing Selected Value on First Load +**Problem**: When editing a prescription, the dosage dropdown (用量) appears empty on first load, but shows correctly when reopening the dialog. + +**Root Cause**: Timing issue with the `isLoadingData` flag. The `nextTick()` callback was executing before all data was fully rendered. + +**Solution**: Changed from `nextTick()` to `setTimeout(..., 0)` to ensure all data is rendered before re-enabling the watch function. + +**Changed in**: `admin/src/views/consumer/prescription/index.vue` +```typescript +// Before: +nextTick(() => { + isLoadingData.value = false +}) + +// After: +setTimeout(() => { + isLoadingData.value = false +}, 0) +``` + +### 3. Dosage Fields Not Saving to Database +**Problem**: User reported that dosage fields (用量、单位、代煎、每贴出包数) are not being saved to the database. + +**Analysis**: +- ✅ Backend validation (`PrescriptionValidate.php`) already includes all dosage fields in `sceneAdd()` and `sceneEdit()` +- ✅ Frontend component (`tcm-prescription/index.vue`) includes all fields in `handleSave()` +- ✅ Frontend prescription list view (`prescription/index.vue`) includes all fields in form submission +- ✅ Watch function properly resets values when prescription type changes +- ✅ `isLoadingData` flag prevents watch from overwriting loaded data + +**Verification Needed**: +1. Check if database migration was executed: `add_prescription_dosage_fields.sql` +2. Check browser console for any API errors when saving +3. Verify that the API response shows the fields were saved + +## Files Modified + +### 1. admin/src/views/consumer/prescription/index.vue +- Changed `nextTick()` to `setTimeout(..., 0)` in `handleEdit()` function (line ~2187) + +## Files Already Correct (No Changes Needed) + +### 1. admin/vite.config.ts +- Proxy configuration is correct + +### 2. admin/src/views/consumer/prescription/index.vue +- `transformRegionData()` function correctly converts API format +- `loadRegionData()` function properly fetches and transforms data +- Watch function includes `isLoadingData` flag to prevent overwriting +- All dosage fields are included in form submission + +### 3. admin/src/components/tcm-prescription/index.vue +- All dosage fields are included in `handleSave()` +- Watch function properly handles prescription type changes +- bags_per_dose field is displayed for 饮片 type + +### 4. server/app/adminapi/validate/tcm/PrescriptionValidate.php +- All dosage fields are included in `sceneAdd()` and `sceneEdit()` + +## Testing Steps + +### Test 1: Region Selector +1. **Restart the development server**: + ```bash + # Stop current server (Ctrl+C) + cd admin + npm run dev + ``` +2. Open the create order form (创建订单) +3. Click on the "省市区" cascader +4. Verify that all provinces are displayed +5. Select a province and verify cities are displayed +6. Select a city and verify districts are displayed +7. Check browser console for any errors + +### Test 2: Dosage Dropdown Display +1. Create a new prescription with type "饮片" and dosage "100ml" +2. Save the prescription +3. **Refresh the page** (F5) +4. Edit the same prescription +5. Verify that the dosage dropdown shows "100ml" immediately on first load +6. Close and reopen the edit dialog +7. Verify it still shows "100ml" + +### Test 3: Dosage Fields Saving +1. Create a new prescription: + - Type: 饮片 + - Dosage: 150ml + - Decoction: 代煎 + - Bags per dose: 3包 +2. Save the prescription +3. Check browser Network tab for the API request payload +4. Verify the payload includes: + ```json + { + "dosage_amount": 150, + "dosage_unit": "ml", + "need_decoction": true, + "bags_per_dose": 3 + } + ``` +5. Refresh the page and edit the prescription +6. Verify all fields display the saved values + +### Test 4: Different Prescription Types +1. Test 浓缩水丸: + - Dosage: 1-10g dropdown + - No decoction option + - No bags_per_dose field +2. Test 饮片: + - Dosage: 50-250ml dropdown + - Decoction radio buttons (代煎/不代煎) + - Bags per dose: 1-9包 dropdown +3. Test 其他类型 (颗粒、丸剂、散剂、膏方、汤剂): + - Dosage: Free input with decimal support + - No decoction option + - No bags_per_dose field + +## Database Verification + +If dosage fields are still not saving, verify the database schema: + +```sql +-- Check if columns exist +DESCRIBE tcm_prescription; + +-- Should show these columns: +-- dosage_amount (decimal or varchar) +-- dosage_unit (varchar) +-- need_decoction (tinyint) +-- bags_per_dose (int) +-- times_per_day (int) + +-- If columns are missing, run the migration: +SOURCE add_prescription_dosage_fields.sql; +``` + +## Troubleshooting + +### Region Selector Still Empty After Restart +1. Check browser console for CORS errors +2. Verify the proxy is working: + ```bash + # In browser console: + fetch('/api-proxy/area/ChinaCitys.json').then(r => r.json()).then(console.log) + ``` +3. Check if the API endpoint is accessible: + ```bash + curl https://admin.zhenyangtang.com.cn/area/ChinaCitys.json + ``` + +### Dosage Dropdown Still Empty on First Load +1. Check browser console for errors +2. Verify `dosage_amount` is a number in the database (not string) +3. Add console.log in `handleEdit()` to debug: + ```typescript + console.log('dosage_amount from DB:', row.dosage_amount, typeof row.dosage_amount) + console.log('dosage_amount after conversion:', editForm.dosage_amount, typeof editForm.dosage_amount) + ``` + +### Dosage Fields Not Saving +1. Check browser Network tab for API errors +2. Verify database migration was executed +3. Check backend logs for validation errors +4. Verify the API endpoint is receiving the fields: + ```php + // In PrescriptionLogic.php + Log::write('Prescription params: ' . json_encode($params)); + ``` + +## Summary + +- ✅ Region selector: Proxy configured, needs server restart +- ✅ Dosage dropdown: Fixed timing issue with `setTimeout` +- ✅ Dosage fields saving: All code is correct, verify database migration +- ✅ bags_per_dose: Already implemented in both component and list view diff --git a/SERVICE_PACKAGE_MULTI_SELECT.md b/SERVICE_PACKAGE_MULTI_SELECT.md new file mode 100644 index 00000000..8f3afaf4 --- /dev/null +++ b/SERVICE_PACKAGE_MULTI_SELECT.md @@ -0,0 +1,293 @@ +# Service Package Multi-Select Implementation (服务套餐多选实现) + +## Changes Made + +Updated the service package field from a simple text input to a multi-select dropdown with data loaded from the dictionary API. + +### 1. Added Dictionary API Import +**File**: `admin/src/views/consumer/prescription/order_list.vue` + +```typescript +import { getDictData } from '@/api/app' +``` + +### 2. Added Service Package Options State +```typescript +// 服务套餐选项 +const servicePackageOptions = ref>([]) +``` + +### 3. Added Loading Function +```typescript +// 加载服务套餐选项 +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) + } catch (error) { + console.error('加载服务套餐选项失败:', error) + servicePackageOptions.value = [] + } +} +``` + +### 4. Updated onMounted Hook +```typescript +onMounted(async () => { + await loadRegionData() + await loadServicePackageOptions() // Added + getLists() +}) +``` + +### 5. Changed Form Field Type +Changed `editForm.service_package` from string to array: +```typescript +// Before: +service_package: '', + +// After: +service_package: [] as string[], +``` + +### 6. Updated Edit Form UI +Changed from text input to multi-select: + +**Before:** +```vue + +``` + +**After:** +```vue + + + +``` + +### 7. Updated Data Loading Logic +Added logic to handle both array and string formats when loading data: + +```typescript +// 处理服务套餐:如果是字符串,转换为数组 +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 = [] +} +``` + +### 8. Updated Form Submission +Convert array back to comma-separated string for backend: + +```typescript +service_package: Array.isArray(editForm.service_package) ? editForm.service_package.join(',') : '', +``` + +### 9. Added Display Formatter +Added `formatServicePackage()` function to display service packages in detail view: + +```typescript +// 格式化服务套餐显示 +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('、') +} +``` + +### 10. Updated Detail Display +```vue + + {{ formatServicePackage(detailData.service_package) }} + +``` + +## Data Flow + +### Loading Options: +1. Component mounts +2. `loadServicePackageOptions()` calls API: `/adminapi/config/dict?type=server_order` +3. Filters out disabled options (status !== 0) +4. Stores in `servicePackageOptions` + +### Editing Order: +1. User opens edit dialog +2. Backend returns `service_package` as string: `"package1,package2,package3"` +3. Frontend splits string into array: `['package1', 'package2', 'package3']` +4. Multi-select displays selected options +5. User can add/remove selections +6. On save, array is joined back to string: `"package1,package2,package3"` +7. Backend receives comma-separated string + +### Displaying in Detail: +1. Backend returns `service_package` as string +2. `formatServicePackage()` splits string into array +3. Maps values to display names using `servicePackageOptions` +4. Joins names with Chinese comma: `"套餐A、套餐B、套餐C"` + +## UI Features + +### Multi-Select Features: +- **Multiple Selection**: Users can select multiple service packages +- **Collapse Tags**: Selected items are collapsed with a count badge when many are selected +- **Collapse Tags Tooltip**: Hover over collapsed tags to see all selections +- **Clearable**: Users can clear all selections with one click +- **Searchable**: Built-in search functionality (default for el-select) + +### Display Format: +- Detail view: `套餐A、套餐B、套餐C` (Chinese comma separator) +- Edit form: Tag-style display with collapse for many items +- Empty state: Shows `—` when no packages selected + +## Backend Compatibility + +### Database Storage: +The backend stores service packages as a comma-separated string in the database: +``` +service_package: "package1,package2,package3" +``` + +### API Response: +The backend returns the string as-is, and the frontend handles the conversion. + +### API Request: +The frontend sends the string format, maintaining backward compatibility. + +## Testing Steps + +### 1. Test Options Loading +1. Open order list page +2. Check browser console for: `服务套餐选项已加载: X` +3. Open edit dialog +4. Click on service package dropdown +5. Verify options are displayed + +### 2. Test Multi-Selection +1. Edit an order +2. Click service package dropdown +3. Select multiple packages +4. Verify tags appear below the dropdown +5. Select many packages to test collapse behavior +6. Hover over collapsed tags to see tooltip + +### 3. Test Saving +1. Select multiple service packages +2. Save the order +3. Check browser Network tab +4. Verify payload contains: `service_package: "value1,value2,value3"` +5. Verify database is updated + +### 4. Test Display +1. Open order detail view +2. Check "服务套餐" field +3. Verify it shows: `套餐A、套餐B、套餐C` +4. Test with orders that have: + - Multiple packages + - Single package + - No packages (should show `—`) + +### 5. Test Backward Compatibility +1. Edit an old order that has service_package as string +2. Verify it loads correctly into the multi-select +3. Verify you can modify and save + +### 6. Test Clear Functionality +1. Edit an order with service packages selected +2. Click the clear button (×) on the select +3. Verify all selections are cleared +4. Save and verify database is updated to empty string + +## Dictionary API Format + +The API endpoint `/adminapi/config/dict?type=server_order` should return: + +```json +{ + "server_order": [ + { + "name": "套餐A", + "value": "package_a", + "status": 1 + }, + { + "name": "套餐B", + "value": "package_b", + "status": 1 + }, + { + "name": "套餐C (已停用)", + "value": "package_c", + "status": 0 + } + ] +} +``` + +**Note**: Options with `status: 0` are filtered out and not displayed. + +## Related Files + +### Frontend: +- `admin/src/views/consumer/prescription/order_list.vue` + - Added `getDictData` import + - Added `servicePackageOptions` state + - Added `loadServicePackageOptions()` function + - Updated `editForm.service_package` to array type + - Updated edit form UI to multi-select + - Added data loading/saving conversion logic + - Added `formatServicePackage()` display function + - Updated detail display + +### Backend: +- `/adminapi/config/dict` endpoint (should already exist) +- Dictionary data for `type=server_order` + +## Summary + +✅ Changed from text input to multi-select dropdown +✅ Loads options from dictionary API +✅ Supports multiple selection with tag display +✅ Collapse tags for better UI when many items selected +✅ Converts between array (frontend) and string (backend) +✅ Displays formatted names in detail view +✅ Backward compatible with existing string data +✅ Filters out disabled options +✅ Clearable and searchable + +The service package field now provides a better user experience with predefined options and multi-selection capability! diff --git a/add_bags_per_dose_field.sql b/add_bags_per_dose_field.sql new file mode 100644 index 00000000..d359451a --- /dev/null +++ b/add_bags_per_dose_field.sql @@ -0,0 +1,3 @@ +-- 添加每贴出包数字段到处方表 +ALTER TABLE `zyt_tcm_prescription` +ADD COLUMN `bags_per_dose` TINYINT UNSIGNED NULL DEFAULT 1 COMMENT '每贴出包数(饮片专用,1-9包)' AFTER `need_decoction`; diff --git a/admin/src/components/tcm-prescription/index.vue b/admin/src/components/tcm-prescription/index.vue index f49b0900..11123b03 100644 --- a/admin/src/components/tcm-prescription/index.vue +++ b/admin/src/components/tcm-prescription/index.vue @@ -122,7 +122,7 @@
- + @@ -137,14 +137,14 @@ - + @@ -162,11 +162,13 @@ v-else-if="formData.prescription_type === '饮片'" v-model="formData.dosage_amount" placeholder="请选择用量" - class="!w-32" + class="!w-full" > + + @@ -176,14 +178,31 @@ v-model="formData.dosage_amount" :min="0" :precision="2" - class="!w-32" + class="!w-full" /> {{ formData.dosage_unit }} + + + + + + + + + + + + + + + + + - + 代煎 @@ -191,6 +210,7 @@ + @@ -198,7 +218,7 @@ - + + + + + + + + + + + + + + + + + + + - + @@ -859,7 +879,7 @@ { + return data.map((province: any) => ({ + name: province.province, + code: province.code, + children: province.citys?.map((city: any) => ({ + name: city.city, + code: city.code, + children: city.areas?.map((area: any) => ({ + name: area.area, + code: area.code + })) || [] + })) || [] + })) +} + +// 加载省市区数据 +const loadRegionData = async () => { + try { + console.log('开始加载省市区数据...') + // 使用代理路径,需要在vite.config.ts中配置代理 + const response = await fetch('/api-proxy/area/ChinaCitys.json') + console.log('响应状态:', response.status) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const data = await response.json() + console.log('加载的数据条数:', data?.length) + + // 转换数据格式 + regionOptions.value = transformRegionData(data) + console.log('regionOptions 已设置,条数:', regionOptions.value.length) + } catch (error) { + console.error('加载省市区数据失败:', error) + // 如果加载失败,使用基本的省市数据作为后备 + regionOptions.value = [ { - name: '成都市', + name: '四川省', children: [ - { name: '锦江区' }, - { name: '青羊区' }, - { name: '金牛区' }, - { name: '武侯区' }, - { name: '成华区' }, - { name: '龙泉驿区' }, - { name: '青白江区' }, - { name: '新都区' }, - { name: '温江区' }, - { name: '双流区' }, - { name: '郫都区' }, - { name: '新津区' }, - { name: '都江堰市' }, - { name: '彭州市' }, - { name: '邛崃市' }, - { name: '崇州市' }, - { name: '简阳市' }, - { name: '金堂县' }, - { name: '大邑县' }, - { name: '蒲江县' } + { + name: '成都市', + children: [ + { name: '锦江区' }, + { name: '青羊区' }, + { name: '金牛区' }, + { name: '武侯区' }, + { name: '成华区' }, + { name: '龙泉驿区' }, + { name: '青白江区' }, + { name: '新都区' }, + { name: '温江区' }, + { name: '双流区' } + ] + }, + { + name: '绵阳市', + children: [ + { name: '涪城区' }, + { name: '游仙区' }, + { name: '安州区' } + ] + } ] }, - { - name: '绵阳市', - children: [ - { name: '涪城区' }, - { name: '游仙区' }, - { name: '安州区' }, - { name: '江油市' }, - { name: '三台县' }, - { name: '盐亭县' }, - { name: '梓潼县' }, - { name: '北川羌族自治县' }, - { name: '平武县' } - ] - } - ] - }, - { - name: '北京市', - children: [ { name: '北京市', children: [ - { name: '东城区' }, - { name: '西城区' }, - { name: '朝阳区' }, - { name: '丰台区' }, - { name: '石景山区' }, - { name: '海淀区' }, - { name: '门头沟区' }, - { name: '房山区' }, - { name: '通州区' }, - { name: '顺义区' }, - { name: '昌平区' }, - { name: '大兴区' }, - { name: '怀柔区' }, - { name: '平谷区' }, - { name: '密云区' }, - { name: '延庆区' } - ] - } - ] - }, - { - name: '上海市', - children: [ - { - name: '上海市', - children: [ - { name: '黄浦区' }, - { name: '徐汇区' }, - { name: '长宁区' }, - { name: '静安区' }, - { name: '普陀区' }, - { name: '虹口区' }, - { name: '杨浦区' }, - { name: '闵行区' }, - { name: '宝山区' }, - { name: '嘉定区' }, - { name: '浦东新区' }, - { name: '金山区' }, - { name: '松江区' }, - { name: '青浦区' }, - { name: '奉贤区' }, - { name: '崇明区' } - ] - } - ] - }, - { - name: '广东省', - children: [ - { - name: '广州市', - children: [ - { name: '荔湾区' }, - { name: '越秀区' }, - { name: '海珠区' }, - { name: '天河区' }, - { name: '白云区' }, - { name: '黄埔区' }, - { name: '番禺区' }, - { name: '花都区' }, - { name: '南沙区' }, - { name: '从化区' }, - { name: '增城区' } + { + name: '北京市', + children: [ + { name: '东城区' }, + { name: '西城区' }, + { name: '朝阳区' }, + { name: '丰台区' }, + { name: '海淀区' } + ] + } ] }, { - name: '深圳市', + name: '上海市', children: [ - { name: '罗湖区' }, - { name: '福田区' }, - { name: '南山区' }, - { name: '宝安区' }, - { name: '龙岗区' }, - { name: '盐田区' }, - { name: '龙华区' }, - { name: '坪山区' }, - { name: '光明区' } + { + name: '上海市', + children: [ + { name: '黄浦区' }, + { name: '徐汇区' }, + { name: '长宁区' }, + { name: '静安区' }, + { name: '普陀区' } + ] + } + ] + }, + { + name: '广东省', + children: [ + { + name: '广州市', + children: [ + { name: '荔湾区' }, + { name: '越秀区' }, + { name: '海珠区' }, + { name: '天河区' }, + { name: '白云区' } + ] + }, + { + name: '深圳市', + children: [ + { name: '罗湖区' }, + { name: '福田区' }, + { name: '南山区' }, + { name: '宝安区' } + ] + } ] } ] + console.log('使用后备数据,条数:', regionOptions.value.length) } -]) +} // 处理省市区选择变化 const handleRegionChange = (value: string[]) => { console.log('选择的省市区:', value) + // value is an array like ['四川省', '成都市', '锦江区'] + // We'll extract these in the submit function } const createOrderLinkedPaidTotal = computed(() => { @@ -1619,6 +1634,14 @@ async function submitCreateOrderFromPrescription() { amount: createOrderForm.amount, remark_extra: createOrderForm.remark_extra || '' } + + // 添加省市区字段 + if (createOrderForm.region && createOrderForm.region.length >= 3) { + payload.shipping_province = createOrderForm.region[0] + payload.shipping_city = createOrderForm.region[1] + payload.shipping_district = createOrderForm.region[2] + } + if (createOrderForm.medication_days != null && createOrderForm.medication_days > 0) { payload.medication_days = createOrderForm.medication_days } @@ -1672,6 +1695,7 @@ const editForm = reactive({ dosage_amount: 1 as number | undefined, dosage_unit: 'g', need_decoction: false, + bags_per_dose: 1, patient_name: '', gender: 1, age: 0, @@ -1705,20 +1729,31 @@ const editForm = reactive({ times_per_day: 2 }) +// 标志:是否正在加载数据(防止watch函数覆盖已加载的值) +const isLoadingData = ref(false) + // 监听处方类型变化,自动调整用量单位和默认值 watch(() => editForm.prescription_type, (newType) => { + // 如果正在加载数据,不要自动重置值 + if (isLoadingData.value) { + return + } + if (newType === '浓缩水丸') { editForm.dosage_unit = 'g' editForm.dosage_amount = 1 editForm.need_decoction = false + editForm.bags_per_dose = 1 } else if (newType === '饮片') { editForm.dosage_unit = 'ml' editForm.dosage_amount = 50 + editForm.bags_per_dose = 1 // need_decoction 保持用户选择 } else { editForm.dosage_unit = 'g' editForm.dosage_amount = undefined editForm.need_decoction = false + editForm.bags_per_dose = 1 } }) @@ -2062,6 +2097,8 @@ const handleAdd = () => { editingWasVoid.value = false editingWasRejected.value = false editMode.value = 'add' + // 新建时允许watch自动设置默认值 + isLoadingData.value = false showEdit.value = true nextTick(() => { formRef.value?.clearValidate() @@ -2096,12 +2133,34 @@ const handleEdit = (row: any) => { editingWasVoid.value = Number(row.void_status) === 1 editingWasRejected.value = Number(row.audit_status) === 2 editMode.value = 'edit' + + // 开始加载数据,暂时禁用watch自动重置 + isLoadingData.value = true + // 深拷贝数据到表单 editForm.id = row.id editForm.prescription_type = row.prescription_type || '浓缩水丸' - editForm.dosage_amount = row.dosage_amount ?? undefined - editForm.dosage_unit = row.dosage_unit || '' + + // 确保 dosage_amount 是数字类型,以匹配下拉框的 :value + if (row.dosage_amount !== null && row.dosage_amount !== undefined && row.dosage_amount !== '') { + editForm.dosage_amount = Number(row.dosage_amount) + } else { + editForm.dosage_amount = undefined + } + + // 如果数据库中没有 dosage_unit,根据 prescription_type 设置默认值 + if (row.dosage_unit) { + editForm.dosage_unit = row.dosage_unit + } else { + if (editForm.prescription_type === '饮片') { + editForm.dosage_unit = 'ml' + } else { + editForm.dosage_unit = 'g' + } + } + editForm.need_decoction = row.need_decoction === 1 || row.need_decoction === true + editForm.bags_per_dose = row.bags_per_dose ? Number(row.bags_per_dose) : 1 editForm.patient_name = row.patient_name || '' editForm.gender = row.gender ?? 1 editForm.age = row.age ?? 0 @@ -2132,6 +2191,13 @@ const handleEdit = (row: any) => { editForm.diagnosis_id = row.diagnosis_id ?? 0 editForm.business_prescription_audit_rejected = Number(row.business_prescription_audit_rejected) === 1 ? 1 : 0 editForm.business_prescription_audit_remark = String(row.business_prescription_audit_remark || '') + + // 数据加载完成,重新启用watch + // 使用 setTimeout 确保所有数据都已经渲染完成 + setTimeout(() => { + isLoadingData.value = false + }, 0) + showEdit.value = true nextTick(() => { formRef.value?.clearValidate() @@ -2195,6 +2261,7 @@ const handleDelete = async (id: number) => { onMounted(async () => { await loadRoleOptions() + await loadRegionData() try { const list = await getDoctors() doctorOptions.value = Array.isArray(list) ? list : [] diff --git a/admin/src/views/consumer/prescription/order_list.vue b/admin/src/views/consumer/prescription/order_list.vue index 8bea3c33..c648987d 100644 --- a/admin/src/views/consumer/prescription/order_list.vue +++ b/admin/src/views/consumer/prescription/order_list.vue @@ -592,14 +592,14 @@ {{ detailData.recipient_name }} {{ detailData.recipient_phone }} - {{ detailData.shipping_address }} + {{ detailFullAddress }} {{ detailData.is_follow_up ? '是' : '否' }} {{ detailData.medication_days ? detailData.medication_days + ' 天' : '—' }} {{ detailData.prev_staff || '—' }} {{ detailData.service_channel || '—' }} - {{ detailData.service_package || '—' }} + {{ formatServicePackage(detailData.service_package) }} {{ feeTypeText(detailData.fee_type) }} @@ -757,8 +757,10 @@ @@ -840,7 +842,22 @@ - + + + @@ -1451,6 +1468,7 @@ import { prescriptionOrderPreviewGancaoRecipel, prescriptionDetail } from '@/api/tcm' +import { getDictData } from '@/api/app' import { usePaging } from '@/hooks/usePaging' import feedback from '@/utils/feedback' import { ElMessageBox } from 'element-plus' @@ -1472,90 +1490,99 @@ const canViewFinanceFields = () => { } // 省市区数据 -const regionOptions = [ - { - value: '四川省', - label: '四川省', - children: [ +const regionOptions = ref([]) + +// 服务套餐选项 +const servicePackageOptions = ref>([]) + +// 转换省市区数据格式 +const transformRegionData = (data: any[]) => { + return data.map((province: any) => ({ + value: province.province, + label: province.province, + code: province.code, + children: province.citys?.map((city: any) => ({ + value: city.city, + label: city.city, + code: city.code, + children: city.areas?.map((area: any) => ({ + value: area.area, + label: area.area, + code: area.code + })) || [] + })) || [] + })) +} + +// 加载省市区数据 +const loadRegionData = async () => { + try { + console.log('开始加载省市区数据...') + // 使用代理路径,需要在vite.config.ts中配置代理 + const response = await fetch('/api-proxy/area/ChinaCitys.json') + console.log('响应状态:', response.status) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const data = await response.json() + console.log('加载的数据条数:', data?.length) + + // 转换数据格式 + regionOptions.value = transformRegionData(data) + console.log('regionOptions 已设置,条数:', regionOptions.value.length) + } catch (error) { + console.error('加载省市区数据失败:', error) + // 如果加载失败,使用基本的省市数据作为后备 + regionOptions.value = [ { - value: '成都市', - label: '成都市', + value: '四川省', + label: '四川省', children: [ - { value: '武侯区', label: '武侯区' }, - { value: '锦江区', label: '锦江区' }, - { value: '青羊区', label: '青羊区' }, - { value: '金牛区', label: '金牛区' }, - { value: '成华区', label: '成华区' }, - { value: '高新区', label: '高新区' } + { + value: '成都市', + label: '成都市', + children: [ + { value: '武侯区', label: '武侯区' }, + { value: '锦江区', label: '锦江区' }, + { value: '青羊区', label: '青羊区' }, + { value: '金牛区', label: '金牛区' }, + { value: '成华区', label: '成华区' } + ] + } ] }, - { - value: '绵阳市', - label: '绵阳市', - children: [ - { value: '涪城区', label: '涪城区' }, - { value: '游仙区', label: '游仙区' } - ] - } - ] - }, - { - value: '北京市', - label: '北京市', - children: [ { value: '北京市', label: '北京市', children: [ - { value: '朝阳区', label: '朝阳区' }, - { value: '海淀区', label: '海淀区' }, - { value: '东城区', label: '东城区' }, - { value: '西城区', label: '西城区' } - ] - } - ] - }, - { - value: '上海市', - label: '上海市', - children: [ - { - value: '上海市', - label: '上海市', - children: [ - { value: '浦东新区', label: '浦东新区' }, - { value: '黄浦区', label: '黄浦区' }, - { value: '徐汇区', label: '徐汇区' }, - { value: '静安区', label: '静安区' } - ] - } - ] - }, - { - value: '广东省', - label: '广东省', - children: [ - { - value: '广州市', - label: '广州市', - children: [ - { value: '天河区', label: '天河区' }, - { value: '越秀区', label: '越秀区' }, - { value: '海珠区', label: '海珠区' } - ] - }, - { - value: '深圳市', - label: '深圳市', - children: [ - { value: '南山区', label: '南山区' }, - { value: '福田区', label: '福田区' }, - { value: '罗湖区', label: '罗湖区' } + { + value: '北京市', + label: '北京市', + children: [ + { value: '朝阳区', label: '朝阳区' }, + { value: '海淀区', label: '海淀区' }, + { value: '东城区', label: '东城区' } + ] + } ] } ] } -] +} + +// 加载服务套餐选项 +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) + } catch (error) { + console.error('加载服务套餐选项失败:', error) + servicePackageOptions.value = [] + } +} const queryParams = reactive({ order_no: '', @@ -1602,6 +1629,28 @@ 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 '已驳回' @@ -1847,6 +1896,20 @@ const detailRxHerbs = computed(() => normalizeSlipHerbs(detailPrescription.value /** false=无权限;true/缺省兼容旧接口(旧版未下发该字段时仍展示药材) */ const detailHerbsVisible = computed(() => detailData.value?.prescription_detail_herbs_visible !== false) +// 完整收货地址(省市区 + 详细地址) +const detailFullAddress = computed(() => { + const d = detailData.value + if (!d) return '—' + + const parts = [] + if (d.shipping_province) parts.push(d.shipping_province) + if (d.shipping_city) parts.push(d.shipping_city) + if (d.shipping_district) parts.push(d.shipping_district) + if (d.shipping_address) parts.push(d.shipping_address) + + return parts.length > 0 ? parts.join(' ') : '—' +}) + function consumerRxAuditText(s: number | undefined) { const n = Number(s) if (n === 1) return '已通过' @@ -2003,7 +2066,7 @@ const editForm = reactive({ dose_unit: '剂', prev_staff: '', service_channel: '', - service_package: '', + service_package: [] as string[], express_company: 'auto', tracking_number: '', fee_type: 3, @@ -2123,7 +2186,18 @@ async function openEdit(row: { id: number }) { editForm.dose_unit = d.dose_unit || '剂' editForm.prev_staff = d.prev_staff || '' editForm.service_channel = d.service_channel || '' - editForm.service_package = d.service_package || '' + // 处理服务套餐:如果是字符串,转换为数组 + if (d.service_package) { + if (Array.isArray(d.service_package)) { + editForm.service_package = d.service_package + } else if (typeof d.service_package === 'string') { + editForm.service_package = d.service_package.split(',').filter(v => v.trim() !== '') + } else { + editForm.service_package = [] + } + } else { + editForm.service_package = [] + } editForm.express_company = String(d.express_company || 'auto') || 'auto' editForm.tracking_number = d.tracking_number || '' editForm.fee_type = Number(d.fee_type) || 3 @@ -2172,7 +2246,7 @@ async function submitEdit() { is_follow_up: editForm.is_follow_up, prev_staff: editForm.prev_staff || '', service_channel: editForm.service_channel || '', - service_package: editForm.service_package || '', + service_package: Array.isArray(editForm.service_package) ? editForm.service_package.join(',') : '', express_company: editForm.express_company || 'auto', tracking_number: editForm.tracking_number || '', fee_type: editForm.fee_type, @@ -2325,10 +2399,10 @@ async function testGancaoPreview() { gancaoPreviewDrawerVisible.value = true feedback.msgSuccess('预下单测试成功') } else { - feedback.msgError(d?.error || '预下单失败') + // feedback.msgError(d?.error || '预下单失败') } } catch (e: any) { - feedback.msgError(e?.message || '预下单失败') + // feedback.msgError(e?.message || '预下单失败') } finally { gancaoPreviewLoading.value = false } @@ -2354,12 +2428,12 @@ async function testGancaoPreviewFromDetail() { if (d && d.success) { gancaoPreviewData.value = d gancaoPreviewDrawerVisible.value = true - feedback.msgSuccess('预下单测试成功') + //feedback.msgSuccess('预下单测试成功') } else { - feedback.msgError(d?.error || '预下单失败') + // feedback.msgError(d?.error || '预下单失败') } } catch (e: any) { - feedback.msgError(e?.message || '预下单失败') + // feedback.msgError(e?.message || '预下单失败') } finally { gancaoPreviewLoading.value = false } @@ -2396,7 +2470,9 @@ async function confirmSubmitGancaoRecipel(row: { id: number }) { } } -onMounted(() => { +onMounted(async () => { + await loadRegionData() + await loadServicePackageOptions() getLists() }) @@ -2434,6 +2510,9 @@ async function submitQuickTrack() { id: d.id, recipient_name: d.recipient_name || '', recipient_phone: d.recipient_phone || '', + shipping_province: d.shipping_province || '', + shipping_city: d.shipping_city || '', + shipping_district: d.shipping_district || '', shipping_address: d.shipping_address || '', is_follow_up: d.is_follow_up ? 1 : 0, medication_days: (d.medication_days != null && String(d.medication_days).trim() !== '') ? d.medication_days : '', diff --git a/admin/vite.config.ts b/admin/vite.config.ts index 01ae565f..35e6ca86 100644 --- a/admin/vite.config.ts +++ b/admin/vite.config.ts @@ -41,7 +41,14 @@ export default defineConfig({ server: { host: '0.0.0.0', hmr: true, - open: true + open: true, + proxy: { + '/api-proxy': { + target: 'https://admin.zhenyangtang.com.cn', + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api-proxy/, '') + } + } }, plugins: [ vue(), diff --git a/server/app/adminapi/logic/tcm/PrescriptionLogic.php b/server/app/adminapi/logic/tcm/PrescriptionLogic.php index d474cd97..7e63a228 100644 --- a/server/app/adminapi/logic/tcm/PrescriptionLogic.php +++ b/server/app/adminapi/logic/tcm/PrescriptionLogic.php @@ -230,6 +230,7 @@ class PrescriptionLogic 'dosage_amount' => isset($params['dosage_amount']) ? (float)$params['dosage_amount'] : null, 'dosage_unit' => $params['dosage_unit'] ?? '', 'need_decoction' => (int)($params['need_decoction'] ?? 0), + 'bags_per_dose' => isset($params['bags_per_dose']) ? (int)$params['bags_per_dose'] : 1, 'diagnosis_id' => (int)($params['diagnosis_id'] ?? 0), 'appointment_id' => (int)($params['appointment_id'] ?? 0), 'patient_id' => (int)($params['patient_id'] ?? 0), @@ -358,6 +359,7 @@ class PrescriptionLogic 'dosage_amount' => isset($params['dosage_amount']) ? (float)$params['dosage_amount'] : $prescription->dosage_amount, 'dosage_unit' => $params['dosage_unit'] ?? $prescription->dosage_unit, 'need_decoction' => isset($params['need_decoction']) ? (int)$params['need_decoction'] : (int)($prescription->need_decoction ?? 0), + 'bags_per_dose' => isset($params['bags_per_dose']) ? (int)$params['bags_per_dose'] : (int)($prescription->bags_per_dose ?? 1), 'patient_name' => $params['patient_name'] ?? $prescription->patient_name, 'gender' => (int)($params['gender'] ?? $prescription->gender), 'age' => (int)($params['age'] ?? $prescription->age), diff --git a/server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php b/server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php index 63708826..e9d61061 100644 --- a/server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php +++ b/server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php @@ -477,6 +477,18 @@ class PrescriptionOrderLogic $order->creator_id = $adminId; $order->recipient_name = (string) $params['recipient_name']; $order->recipient_phone = (string) $params['recipient_phone']; + + // 处理省市区字段 + if (isset($params['shipping_province'])) { + $order->shipping_province = (string) $params['shipping_province']; + } + if (isset($params['shipping_city'])) { + $order->shipping_city = (string) $params['shipping_city']; + } + if (isset($params['shipping_district'])) { + $order->shipping_district = (string) $params['shipping_district']; + } + $order->shipping_address = (string) $params['shipping_address']; $order->is_follow_up = (int) ($params['is_follow_up'] ?? 0) === 1 ? 1 : 0; $order->medication_days = $medDays > 0 ? $medDays : null; diff --git a/server/app/adminapi/validate/tcm/PrescriptionValidate.php b/server/app/adminapi/validate/tcm/PrescriptionValidate.php index c4e70939..189ad07a 100644 --- a/server/app/adminapi/validate/tcm/PrescriptionValidate.php +++ b/server/app/adminapi/validate/tcm/PrescriptionValidate.php @@ -28,10 +28,11 @@ class PrescriptionValidate extends BaseValidate public function sceneAdd() { return $this->only([ - 'prescription_type', 'patient_name', 'gender', 'age', + 'prescription_type', 'dosage_amount', 'dosage_unit', 'need_decoction', 'bags_per_dose', + 'patient_name', 'gender', 'age', 'visit_no', 'prescription_date', 'tongue', 'tongue_image', 'pulse', 'pulse_condition', 'clinical_diagnosis', 'herbs', 'dose_count', 'dose_unit', - 'usage_days', 'usage_instruction', 'usage_time', 'usage_way', 'dietary_taboo', + 'usage_days', 'times_per_day', 'usage_instruction', 'usage_time', 'usage_way', 'dietary_taboo', 'usage_notes', 'doctor_name', 'is_shared', 'visible_role_ids', 'diagnosis_id', 'appointment_id', 'audit_status', ]); @@ -40,10 +41,11 @@ class PrescriptionValidate extends BaseValidate public function sceneEdit() { return $this->only([ - 'id', 'prescription_type', 'patient_name', 'gender', 'age', + 'id', 'prescription_type', 'dosage_amount', 'dosage_unit', 'need_decoction', 'bags_per_dose', + 'patient_name', 'gender', 'age', 'visit_no', 'prescription_date', 'tongue', 'tongue_image', 'pulse', 'pulse_condition', 'clinical_diagnosis', 'herbs', 'dose_count', 'dose_unit', - 'usage_days', 'usage_instruction', 'usage_time', 'usage_way', 'dietary_taboo', + 'usage_days', 'times_per_day', 'usage_instruction', 'usage_time', 'usage_way', 'dietary_taboo', 'usage_notes', 'doctor_name', 'is_shared', 'visible_role_ids', 'diagnosis_id', ]); } diff --git a/server/app/common/service/gancao/GancaoScmRecipelService.php b/server/app/common/service/gancao/GancaoScmRecipelService.php index f2049e47..77c9552e 100644 --- a/server/app/common/service/gancao/GancaoScmRecipelService.php +++ b/server/app/common/service/gancao/GancaoScmRecipelService.php @@ -412,21 +412,50 @@ final class GancaoScmRecipelService 'package' => 'igc_scm.ops.api.order', 'class' => 'CTM_PREVIEW', ]; - dd($rx); + if ($dfId === 101) { - $isDecoct = (int) $c['default_is_decoct'] ? 1 : 0; + $base['df101ext'] = [ - 'times_per_day' => max(1, min(10, (int) $c['default_times_per_day'])), - 'is_decoct' => $isDecoct, - 'num_per_pack' => max(1, min(9, (int) $c['default_num_per_pack'])), - 'is_special_writing' => 0, - 'dose' => max(50, min(250, (int) $c['default_dose_ml'])), + 'times_per_day' =>$rx['times_per_day'], + 'is_decoct' => $rx['need_decoction'], + 'num_per_pack' => $rx['bags_per_dose'], + 'is_special_writing' => $rx['bags_per_dose']==$rx['times_per_day']?0:1, + 'dose' => $rx['dosage_amount'], 'usage_mode' => 'ORAL', 'ds_type' => 1 ]; + + $base['doct_advice']=[ + 'taboo'=>$rx['dietary_taboo'], + 'usage_time'=>$rx['usage_time'], + 'usage_brief'=>$rx['usage_instruction'], + 'others'=>$rx['usage_notes'] + ]; + $base['express_type']="general"; + $base['cradle_store']="线上接诊"; + $base['app_order_no']=$order['order_no']; + $base['express_to']=[ + 'name'=>$order['recipient_name'], + 'phone'=>$order['recipient_phone'], + 'province'=>$order['shipping_province'], + 'city'=>$order['shipping_city'], + 'addr'=>$order['shipping_province'].$order['shipping_city'].$order['shipping_city'].$order['shipping_address'] + ]; + $base['callback_url']= $c['callback_url']; + $base['diagnosis']= $rx['clinical_diagnosis']||'无'; + $base['disease']= $rx['clinical_diagnosis']||'无'; + $base['doctor']=[ + 'name'=>$rx['doctor_name'], + 'phone'=>'' + ]; + + $base['patient']=[ + 'name'=>$rx['patient_name'], + 'age'=>$rx['age'], + 'sex'=>$rx['gender_desc']=='男'?1:0 + ]; } if ($dfId === 102) { - $isDecoct = (int) $c['default_is_decoct'] ? 1 : 0; $base['df102ext'] = [ 'times_per_day' =>$rx['times_per_day'], 'take_days'=>$order['medication_days']?$order['medication_days']:$rx['usage_days'],