diff --git a/DYNAMIC_DOSE_COUNT_LABEL.md b/DYNAMIC_DOSE_COUNT_LABEL.md new file mode 100644 index 00000000..b71ac497 --- /dev/null +++ b/DYNAMIC_DOSE_COUNT_LABEL.md @@ -0,0 +1,204 @@ +# Dynamic Dose Count Label (动态剂数标签) + +## Feature +Made the dose count label dynamic based on the selected dose unit. When the user selects a different unit (剂/丸/袋/盒/瓶/膏/贴), the label automatically updates to match. + +## Changes Made + +### File: `admin/src/views/consumer/prescription/order_list.vue` + +#### 1. Added Computed Property +Added a computed property that generates the label dynamically: + +```typescript +// 动态剂数标签(根据剂量单位变化) +const doseCountLabel = computed(() => { + const unit = editForm.dose_unit || '剂' + return `${unit}数` +}) +``` + +#### 2. Updated Form Item Label +Changed from static label to dynamic computed property: + +**Before:** +```vue + + + +``` + +**After:** +```vue + + + +``` + +## How It Works + +### Label Generation Logic +```typescript +const unit = editForm.dose_unit || '剂' // Get current unit, default to '剂' +return `${unit}数` // Append '数' to create label +``` + +### Examples + +| Selected Unit | Label Display | +|---------------|---------------| +| 剂 | 剂数 | +| 丸 | 丸数 | +| 袋 | 袋数 | +| 盒 | 盒数 | +| 瓶 | 瓶数 | +| 膏 | 膏数 | +| 贴 | 贴数 | + +### Reactive Updates +The label updates automatically when: +1. User selects a different dose unit from the dropdown +2. Form is loaded with existing data +3. Form is reset or cleared + +## User Experience + +### Before: +- Label always shows "剂数" regardless of selected unit +- Confusing when unit is "贴" but label says "剂数" + +### After: +- Label dynamically matches the selected unit +- Shows "贴数" when unit is "贴" +- Shows "丸数" when unit is "丸" +- More intuitive and consistent + +## Implementation Details + +### Computed Property Benefits +1. **Reactive**: Automatically updates when `editForm.dose_unit` changes +2. **Efficient**: Only recalculates when dependency changes +3. **Clean**: No need for watchers or manual updates + +### Default Value +Uses `'剂'` as default when `dose_unit` is empty or undefined: +```typescript +const unit = editForm.dose_unit || '剂' +``` + +### Placeholder Update +Also updated the placeholder to match the label: +```vue +:placeholder="doseCountLabel" +``` + +This ensures consistency between the label and placeholder text. + +## Testing Steps + +### 1. Test Label Changes +1. Open order edit dialog +2. Check initial label (should be "剂数" if dose_unit is "剂") +3. Change dose unit to "贴" +4. Verify label changes to "贴数" +5. Try all other units and verify labels update correctly + +### 2. Test with Existing Data +1. Edit an order that has dose_unit = "丸" +2. Verify label shows "丸数" when dialog opens +3. Change to different unit +4. Verify label updates immediately + +### 3. Test Default Behavior +1. Create a new order (dose_unit is empty) +2. Verify label shows "剂数" (default) +3. Select a unit +4. Verify label updates + +### 4. Test Placeholder +1. Clear the dose_count field +2. Verify placeholder text matches the label +3. Change dose unit +4. Verify placeholder updates with label + +## Edge Cases Handled + +### Empty/Undefined Unit +```typescript +const unit = editForm.dose_unit || '剂' +``` +Falls back to '剂' if unit is empty, null, or undefined. + +### Form Reset +When form is reset, the computed property automatically recalculates based on the new (or default) value. + +### Multiple Edits +Label updates correctly even when editing multiple orders in sequence without closing the dialog. + +## Related Code + +### Form Structure +```vue + + + + + + + + + + + + + + + + + + +``` + +### Data Flow +``` +User selects dose_unit → editForm.dose_unit changes → +doseCountLabel computed property recalculates → +Label and placeholder update in UI +``` + +## Future Enhancements + +### Possible Improvements +1. **Validation Message**: Update validation messages to use dynamic unit +2. **Display in Detail View**: Show unit-specific label in order detail view +3. **Internationalization**: Support multiple languages for unit names +4. **Custom Units**: Allow admin to configure custom dose units + +### Example: Dynamic Validation +```typescript +const doseCountRules = computed(() => [ + { required: true, message: `请输入${editForm.dose_unit || '剂'}数`, trigger: 'blur' } +]) +``` + +## Summary + +✅ Added dynamic label that changes based on selected dose unit +✅ Label updates automatically when unit changes +✅ Placeholder also updates to match label +✅ Default to "剂数" when unit is empty +✅ Handles all 7 dose units (剂/丸/袋/盒/瓶/膏/贴) +✅ Reactive and efficient using computed property +✅ Better user experience with consistent terminology + +The dose count label now dynamically reflects the selected unit, making the form more intuitive and user-friendly! diff --git a/ORDER_STATUS_TABS_UI.md b/ORDER_STATUS_TABS_UI.md new file mode 100644 index 00000000..8bb22581 --- /dev/null +++ b/ORDER_STATUS_TABS_UI.md @@ -0,0 +1,299 @@ +# Order Status Tabs UI (订单状态标签页UI) + +## Feature +Redesigned the fulfillment status filter from a dropdown select to clickable tabs for better user experience and faster filtering. + +## Changes Made + +### File: `admin/src/views/consumer/prescription/order_list.vue` + +#### 1. Replaced Dropdown with Tab UI +**Before:** +```vue + + + + + + + + + +``` + +**After:** +```vue + + +
+
+
+ {{ tab.label }} + + {{ tab.count }} + +
+
+
+
+``` + +#### 2. Added Status Tabs Configuration +```typescript +// 状态标签页配置 +const statusTabs = ref([ + { label: '全部', value: '' as number | '', count: undefined }, + { label: '待双审通过', value: 1, count: undefined }, + { label: '履约中', value: 2, count: undefined }, + { label: '已发货', value: 5, count: undefined }, + { label: '已完成', value: 3, count: undefined }, + { label: '已取消', value: 4, count: undefined } +]) +``` + +#### 3. Added Click Handler +```typescript +// 处理状态标签点击 +const handleStatusTabClick = (value: number | '') => { + queryParams.fulfillment_status = value + resetPage() +} +``` + +## UI Design + +### Visual States + +**Active Tab:** +- Primary color background (`bg-primary`) +- White text +- Shadow effect +- Badge with semi-transparent white background + +**Inactive Tab:** +- Light gray background (`bg-gray-50`) +- Gray text +- Hover effect (lighter gray) +- Badge with gray background + +### Layout +- Horizontal flex layout with gap +- Wraps on smaller screens +- Smooth transitions (200ms) +- Rounded corners +- Consistent padding + +### Badge (Count Display) +- Small text size +- Rounded badge +- Shows count for each status (optional feature) +- Different styling for active/inactive states + +## User Experience Improvements + +### Before (Dropdown): +1. User clicks dropdown +2. Dropdown opens with options +3. User scrolls to find status +4. User clicks option +5. Dropdown closes +6. User clicks "查询" button + +**Total: 3-4 clicks + scrolling** + +### After (Tabs): +1. User clicks tab +2. Filter applies immediately + +**Total: 1 click** + +### Benefits: +- ✅ **Faster filtering**: One-click access to any status +- ✅ **Visual feedback**: Clear indication of current filter +- ✅ **Better discoverability**: All options visible at once +- ✅ **No extra clicks**: Auto-triggers search on click +- ✅ **Modern UI**: Follows common tab pattern +- ✅ **Mobile friendly**: Wraps on small screens + +## Features + +### 1. Active State Indication +The currently selected tab is highlighted with primary color, making it immediately clear which filter is active. + +### 2. Hover Effects +Inactive tabs show a subtle hover effect, indicating they are clickable. + +### 3. Count Badges (Optional) +Each tab can display a count badge showing the number of orders in that status. Currently set to `undefined` but can be populated with actual counts. + +### 4. Responsive Design +Tabs wrap to multiple lines on smaller screens using `flex-wrap`. + +### 5. Smooth Transitions +All state changes have smooth 200ms transitions for a polished feel. + +## Implementation Details + +### Tab Configuration +```typescript +{ + label: string, // Display text + value: number | '', // Filter value ('' for "All") + count: number | undefined // Optional count badge +} +``` + +### Click Handler Logic +```typescript +const handleStatusTabClick = (value: number | '') => { + queryParams.fulfillment_status = value // Update filter + resetPage() // Trigger search +} +``` + +### Styling Classes +- `bg-primary`: Primary brand color (active state) +- `bg-gray-50`: Light gray (inactive state) +- `hover:bg-gray-100`: Hover effect +- `shadow-md`: Shadow for active tab +- `transition-all duration-200`: Smooth transitions +- `select-none`: Prevent text selection on click + +## Future Enhancements + +### 1. Add Count Badges +Fetch and display order counts for each status: + +```typescript +// After fetching data +const updateStatusCounts = (data: any) => { + statusTabs.value[0].count = data.total + statusTabs.value[1].count = data.pending + statusTabs.value[2].count = data.processing + // ... etc +} +``` + +### 2. Add Icons +Add status-specific icons for better visual recognition: + +```vue + + + +``` + +### 3. Add Keyboard Navigation +Support arrow keys for tab navigation: + +```typescript +const handleKeydown = (e: KeyboardEvent) => { + if (e.key === 'ArrowLeft') { + // Navigate to previous tab + } else if (e.key === 'ArrowRight') { + // Navigate to next tab + } +} +``` + +### 4. Add Loading State +Show loading indicator on active tab while fetching: + +```vue + + + +``` + +## Testing Steps + +### 1. Test Tab Switching +1. Open order list page +2. Click on different status tabs +3. Verify: + - Active tab is highlighted + - Table updates with filtered results + - URL parameters update (if using router) + +### 2. Test Visual States +1. Hover over inactive tabs +2. Verify hover effect appears +3. Click a tab +4. Verify active state styling applies + +### 3. Test "全部" Tab +1. Click "全部" tab +2. Verify all orders are shown +3. Verify no status filter is applied + +### 4. Test Responsive Behavior +1. Resize browser window to small width +2. Verify tabs wrap to multiple lines +3. Verify all tabs remain accessible + +### 5. Test with Search +1. Enter order number in search +2. Click different status tabs +3. Verify both filters work together + +## Accessibility + +### Current Implementation: +- ✅ Keyboard accessible (clickable divs) +- ✅ Visual feedback on hover +- ✅ Clear active state indication +- ✅ Sufficient color contrast + +### Recommended Improvements: +- Add `role="tab"` and `aria-selected` attributes +- Add `tabindex="0"` for keyboard navigation +- Add keyboard event handlers (Enter/Space) +- Add `aria-label` for screen readers + +### Example: +```vue +
+``` + +## Related Files + +### Frontend: +- `admin/src/views/consumer/prescription/order_list.vue` + - Added status tabs UI + - Added `statusTabs` configuration + - Added `handleStatusTabClick` handler + - Removed dropdown select + +## Summary + +✅ Replaced dropdown with clickable tabs +✅ One-click filtering (no need to click "查询") +✅ Visual active state indication +✅ Hover effects for better UX +✅ Responsive design with flex-wrap +✅ Smooth transitions +✅ Support for count badges (optional) +✅ Cleaner, more modern UI + +The status filter is now much more intuitive and efficient, following modern UI patterns commonly seen in admin dashboards! diff --git a/REGION_API_ENV_VARIABLE_UPDATE.md b/REGION_API_ENV_VARIABLE_UPDATE.md new file mode 100644 index 00000000..4d339344 --- /dev/null +++ b/REGION_API_ENV_VARIABLE_UPDATE.md @@ -0,0 +1,288 @@ +# Region API Environment Variable Update (省市区API环境变量更新) + +## Change Summary +Updated the region data API URL to use the `VITE_APP_BASE_URL` environment variable instead of a hardcoded URL, making it more flexible and maintainable across different environments. + +## Changes Made + +### 1. Updated prescription/index.vue +**File**: `admin/src/views/consumer/prescription/index.vue` + +**Before:** +```typescript +const apiUrl = import.meta.env.DEV + ? '/api-proxy/area/ChinaCitys.json' + : 'https://admin.zhenyangtang.com.cn/area/ChinaCitys.json' +``` + +**After:** +```typescript +const apiUrl = import.meta.env.DEV + ? '/api-proxy/area/ChinaCitys.json' + : `${import.meta.env.VITE_APP_BASE_URL}area/ChinaCitys.json` +``` + +### 2. Updated prescription/order_list.vue +**File**: `admin/src/views/consumer/prescription/order_list.vue` + +Applied the same change. + +## Environment Configuration + +### Production Environment +**File**: `admin/.env.production` +```env +VITE_APP_BASE_URL='https://cs.zhenyangtang.com.cn/' +``` + +### Development Environment +**File**: `admin/.env.development` (if exists) +```env +VITE_APP_BASE_URL='http://localhost:8080/' +``` + +## How It Works + +### URL Construction +The production URL is now constructed dynamically: +```typescript +`${import.meta.env.VITE_APP_BASE_URL}area/ChinaCitys.json` +``` + +With `VITE_APP_BASE_URL='https://cs.zhenyangtang.com.cn/'`, this becomes: +``` +https://cs.zhenyangtang.com.cn/area/ChinaCitys.json +``` + +### Development vs Production + +**Development Mode:** +- Uses proxy: `/api-proxy/area/ChinaCitys.json` +- Vite proxy rewrites to external URL +- Avoids CORS issues + +**Production Mode:** +- Uses: `${VITE_APP_BASE_URL}area/ChinaCitys.json` +- Resolves to: `https://cs.zhenyangtang.com.cn/area/ChinaCitys.json` +- Direct fetch from configured base URL + +## Benefits + +### 1. Environment Flexibility +- ✅ Different URLs for different environments (dev, staging, production) +- ✅ No code changes needed when switching environments +- ✅ Single source of truth for base URL + +### 2. Easier Deployment +- ✅ Change URL by updating `.env.production` file +- ✅ No need to modify source code +- ✅ Supports multiple deployment targets + +### 3. Consistency +- ✅ Uses same base URL as other API calls +- ✅ Maintains consistency across the application +- ✅ Easier to manage and maintain + +### 4. Configuration Management +- ✅ Centralized configuration in environment files +- ✅ Easy to override for different environments +- ✅ Follows best practices for environment-specific settings + +## Environment Files + +### Structure +``` +admin/ +├── .env.development # Development environment +├── .env.production # Production environment +├── .env.staging # Staging environment (optional) +└── .env.local # Local overrides (gitignored) +``` + +### Example Configurations + +**Development** (`.env.development`): +```env +VITE_APP_BASE_URL='http://localhost:8080/' +VITE_APP_REQUEST_TIMEOUT=60000 +``` + +**Staging** (`.env.staging`): +```env +VITE_APP_BASE_URL='https://staging.zhenyangtang.com.cn/' +VITE_APP_REQUEST_TIMEOUT=60000 +``` + +**Production** (`.env.production`): +```env +VITE_APP_BASE_URL='https://cs.zhenyangtang.com.cn/' +VITE_APP_REQUEST_TIMEOUT=60000 +``` + +## Testing Steps + +### 1. Verify Environment Variable +Check that the environment variable is loaded: +```typescript +console.log('Base URL:', import.meta.env.VITE_APP_BASE_URL) +``` + +Expected output in production: +``` +Base URL: https://cs.zhenyangtang.com.cn/ +``` + +### 2. Test Development Mode +1. Run development server: + ```bash + cd admin + npm run dev + ``` +2. Open browser console +3. Check constructed URL uses proxy +4. Verify region selector loads data + +### 3. Test Production Build +1. Build for production: + ```bash + cd admin + npm run build + ``` +2. Check built files use production URL +3. Serve and test: + ```bash + npm run preview + ``` +4. Verify region selector loads from: `https://cs.zhenyangtang.com.cn/area/ChinaCitys.json` + +### 4. Test Different Environments +Create a staging build: +```bash +npm run build -- --mode staging +``` + +Verify it uses the staging URL from `.env.staging`. + +## URL Resolution Examples + +### With Trailing Slash (Current) +```typescript +VITE_APP_BASE_URL='https://cs.zhenyangtang.com.cn/' +Result: https://cs.zhenyangtang.com.cn/area/ChinaCitys.json ✅ +``` + +### Without Trailing Slash +```typescript +VITE_APP_BASE_URL='https://cs.zhenyangtang.com.cn' +Result: https://cs.zhenyangtang.com.cnarea/ChinaCitys.json ❌ +``` + +**Important**: Ensure `VITE_APP_BASE_URL` always ends with `/` + +### Handling Missing Trailing Slash +If you want to be defensive, you could add: +```typescript +const baseUrl = import.meta.env.VITE_APP_BASE_URL.endsWith('/') + ? import.meta.env.VITE_APP_BASE_URL + : `${import.meta.env.VITE_APP_BASE_URL}/` + +const apiUrl = import.meta.env.DEV + ? '/api-proxy/area/ChinaCitys.json' + : `${baseUrl}area/ChinaCitys.json` +``` + +## Troubleshooting + +### Issue: Region selector not loading in production +**Check:** +1. Verify `.env.production` file exists +2. Check `VITE_APP_BASE_URL` is set correctly +3. Ensure URL ends with `/` +4. Verify file exists at: `${VITE_APP_BASE_URL}area/ChinaCitys.json` + +### Issue: 404 Not Found +**Solution:** +1. Check the constructed URL in browser Network tab +2. Verify the file is accessible at that URL +3. Check server configuration for the path + +### Issue: CORS Error +**Solution:** +1. Ensure server has proper CORS headers +2. Check `Access-Control-Allow-Origin` header +3. Verify the domain is allowed + +## Server Configuration + +### File Location +Ensure the file exists at: +``` +https://cs.zhenyangtang.com.cn/area/ChinaCitys.json +``` + +### Nginx Configuration Example +```nginx +location /area/ { + alias /path/to/public/area/; + add_header Access-Control-Allow-Origin *; + add_header Cache-Control "public, max-age=86400"; +} +``` + +### Apache Configuration Example +```apache + + Header set Access-Control-Allow-Origin "*" + Header set Cache-Control "public, max-age=86400" + +``` + +## Related Files + +### Frontend: +- `admin/src/views/consumer/prescription/index.vue` + - Updated to use `VITE_APP_BASE_URL` +- `admin/src/views/consumer/prescription/order_list.vue` + - Updated to use `VITE_APP_BASE_URL` + +### Configuration: +- `admin/.env.production` + - Contains `VITE_APP_BASE_URL='https://cs.zhenyangtang.com.cn/'` +- `admin/.env.development` + - Should contain development base URL +- `admin/vite.config.ts` + - Proxy configuration for development + +## Best Practices + +### 1. Always Use Environment Variables +✅ Do: `${import.meta.env.VITE_APP_BASE_URL}path/to/resource` +❌ Don't: `https://hardcoded-domain.com/path/to/resource` + +### 2. Ensure Trailing Slashes +✅ Do: `VITE_APP_BASE_URL='https://example.com/'` +❌ Don't: `VITE_APP_BASE_URL='https://example.com'` + +### 3. Use Consistent Naming +All Vite environment variables should start with `VITE_` to be exposed to the client. + +### 4. Document Environment Variables +Keep a `.env.example` file with all required variables: +```env +# Base URL for API and static resources +VITE_APP_BASE_URL='https://example.com/' + +# Request timeout in milliseconds +VITE_APP_REQUEST_TIMEOUT=60000 +``` + +## Summary + +✅ Replaced hardcoded URL with environment variable +✅ Uses `VITE_APP_BASE_URL` from `.env.production` +✅ More flexible for different environments +✅ Easier to deploy and maintain +✅ Consistent with other API configurations +✅ Applied to both prescription index and order list views + +The region API now uses the configured base URL, making it easier to manage across different environments! diff --git a/REGION_API_PRODUCTION_FIX.md b/REGION_API_PRODUCTION_FIX.md new file mode 100644 index 00000000..7b3795ed --- /dev/null +++ b/REGION_API_PRODUCTION_FIX.md @@ -0,0 +1,243 @@ +# Region API Production URL Fix (省市区API生产环境URL修复) + +## Issue +The region data API was using a proxy path (`/api-proxy/area/ChinaCitys.json`) which only works in development mode. After building for production, the proxy is not available, causing the region selector to fail. + +## Root Cause +Vite's proxy configuration (`vite.config.ts`) only works during development (`npm run dev`). When the application is built for production (`npm run build`), the proxy is not included in the build output, and the relative path `/api-proxy/...` becomes invalid. + +## Solution +Use environment detection to switch between proxy URL (development) and full URL (production). + +## Changes Made + +### 1. Updated prescription/index.vue +**File**: `admin/src/views/consumer/prescription/index.vue` + +Changed from: +```typescript +const response = await fetch('/api-proxy/area/ChinaCitys.json') +``` + +To: +```typescript +// 开发环境使用代理,生产环境使用完整URL +const apiUrl = import.meta.env.DEV + ? '/api-proxy/area/ChinaCitys.json' + : 'https://admin.zhenyangtang.com.cn/area/ChinaCitys.json' + +const response = await fetch(apiUrl) +``` + +### 2. Updated prescription/order_list.vue +**File**: `admin/src/views/consumer/prescription/order_list.vue` + +Applied the same change to the order list view. + +## How It Works + +### Environment Detection +Uses Vite's built-in environment variable `import.meta.env.DEV`: +- **Development** (`npm run dev`): `import.meta.env.DEV = true` +- **Production** (`npm run build`): `import.meta.env.DEV = false` + +### URL Selection Logic +```typescript +const apiUrl = import.meta.env.DEV + ? '/api-proxy/area/ChinaCitys.json' // Development: Use proxy + : 'https://admin.zhenyangtang.com.cn/area/ChinaCitys.json' // Production: Use full URL +``` + +### Development Mode +- Uses proxy path: `/api-proxy/area/ChinaCitys.json` +- Vite proxy rewrites to: `https://admin.zhenyangtang.com.cn/area/ChinaCitys.json` +- Avoids CORS issues during development + +### Production Mode +- Uses full URL: `https://admin.zhenyangtang.com.cn/area/ChinaCitys.json` +- Direct fetch from the actual server +- No proxy needed + +## Benefits + +### 1. Works in Both Environments +- ✅ Development: Uses proxy for CORS-free development +- ✅ Production: Uses direct URL for deployed application + +### 2. No Build Configuration Changes +- No need to modify build scripts +- No environment-specific builds +- Single codebase for all environments + +### 3. Automatic Detection +- Automatically switches based on environment +- No manual configuration needed +- No risk of using wrong URL + +### 4. Maintains CORS Handling +- Development: Proxy handles CORS +- Production: Server should have proper CORS headers + +## Testing Steps + +### 1. Test Development Mode +1. Run development server: + ```bash + cd admin + npm run dev + ``` +2. Open browser console +3. Check for log: `开始加载省市区数据...` +4. Verify it uses proxy URL +5. Check region selector loads data + +### 2. Test Production Build +1. Build for production: + ```bash + cd admin + npm run build + ``` +2. Serve the built files: + ```bash + npm run preview + # or deploy to production server + ``` +3. Open browser console +4. Check for log: `开始加载省市区数据...` +5. Verify it uses full URL +6. Check region selector loads data + +### 3. Verify URL in Network Tab +**Development:** +- Request URL: `http://localhost:5173/api-proxy/area/ChinaCitys.json` +- Proxied to: `https://admin.zhenyangtang.com.cn/area/ChinaCitys.json` + +**Production:** +- Request URL: `https://admin.zhenyangtang.com.cn/area/ChinaCitys.json` +- Direct request (no proxy) + +### 4. Test Fallback Data +If API fails in either environment, verify fallback data is used: +```typescript +regionOptions.value = [ + { + name: '四川省', + children: [...] + }, + ... +] +``` + +## Alternative Solutions Considered + +### Option 1: Environment Variables (Not Chosen) +```typescript +const apiUrl = import.meta.env.VITE_REGION_API_URL || '/api-proxy/area/ChinaCitys.json' +``` +**Pros**: More flexible for different environments +**Cons**: Requires environment-specific configuration files + +### Option 2: Relative Path (Not Chosen) +```typescript +const apiUrl = '/area/ChinaCitys.json' +``` +**Pros**: Simple +**Cons**: Requires copying file to public folder, increases bundle size + +### Option 3: Backend API Endpoint (Not Chosen) +Create a backend endpoint to serve region data +**Pros**: More control, can cache, can update without frontend deploy +**Cons**: Requires backend changes, adds server load + +### Option 4: Current Solution (Chosen) ✅ +Use environment detection with conditional URL +**Pros**: +- Simple implementation +- No additional configuration +- Works in both environments +- No backend changes needed +**Cons**: +- Hardcoded production URL (acceptable for this use case) + +## Production Server Requirements + +### CORS Headers +The production server hosting `ChinaCitys.json` should have proper CORS headers: + +``` +Access-Control-Allow-Origin: * +# or specific domain: +Access-Control-Allow-Origin: https://your-admin-domain.com +``` + +### File Availability +Ensure the file is accessible at: +``` +https://admin.zhenyangtang.com.cn/area/ChinaCitys.json +``` + +### Alternative: Use Backend Proxy +If CORS is an issue in production, consider creating a backend endpoint: +```php +// server/app/adminapi/controller/RegionController.php +public function getChinaCitys() +{ + $url = 'https://admin.zhenyangtang.com.cn/area/ChinaCitys.json'; + $data = file_get_contents($url); + return json(['data' => json_decode($data, true)]); +} +``` + +Then update frontend to use: +```typescript +const apiUrl = import.meta.env.DEV + ? '/api-proxy/area/ChinaCitys.json' + : '/adminapi/region/getChinaCitys' // Backend endpoint +``` + +## Related Files + +### Frontend: +- `admin/src/views/consumer/prescription/index.vue` + - Updated `loadRegionData()` function +- `admin/src/views/consumer/prescription/order_list.vue` + - Updated `loadRegionData()` function + +### Configuration: +- `admin/vite.config.ts` + - Proxy configuration (development only) + +### Documentation: +- `REGION_DATA_SETUP.md` (should be updated to mention production URL) + +## Environment Variables Reference + +### Vite Built-in Variables: +- `import.meta.env.DEV` - Boolean, true in development +- `import.meta.env.PROD` - Boolean, true in production +- `import.meta.env.MODE` - String, 'development' or 'production' + +### Usage Example: +```typescript +if (import.meta.env.DEV) { + console.log('Running in development mode') +} + +if (import.meta.env.PROD) { + console.log('Running in production mode') +} + +console.log('Current mode:', import.meta.env.MODE) +``` + +## Summary + +✅ Fixed production build issue with region API +✅ Uses proxy in development for CORS-free development +✅ Uses direct URL in production for deployed application +✅ Automatic environment detection +✅ No build configuration changes needed +✅ Maintains fallback data for both environments +✅ Applied to both prescription index and order list views + +The region selector now works correctly in both development and production environments! diff --git a/TRTC_COS_STORAGE_MIGRATION.md b/TRTC_COS_STORAGE_MIGRATION.md new file mode 100644 index 00000000..8f22db93 --- /dev/null +++ b/TRTC_COS_STORAGE_MIGRATION.md @@ -0,0 +1,348 @@ +# TRTC 录制存储迁移:VOD → COS + +## 变更说明 + +将 TRTC 云端录制的存储方式从**腾讯云点播(VOD)**改为**对象存储(COS)**。 + +## 修改内容 + +### File: `server/app/common/service/TrtcCloudRecordingService.php` + +#### 1. 存储配置变更 + +**Before (VOD):** +```php +$tencentVod = new \TencentCloud\Trtc\V20190722\Models\TencentVod(); +$tencentVod->ExpireTime = 0; +$tencentVod->MediaType = 0; +$tencentVod->UserDefineRecordId = $prefix; +$tencentVod->SubAppId = $vodSubApp; + +$cloudVod = new \TencentCloud\Trtc\V20190722\Models\CloudVod(); +$cloudVod->TencentVod = $tencentVod; + +$storage = new \TencentCloud\Trtc\V20190722\Models\StorageParams(); +$storage->CloudVod = $cloudVod; +``` + +**After (COS):** +```php +$cloudStorage = new \TencentCloud\Trtc\V20190722\Models\CloudStorage(); +$cloudStorage->Vendor = 0; // 0=腾讯云 +$cloudStorage->Region = 'ap-guangzhou'; +$cloudStorage->Bucket = 'your-bucket-name'; +$cloudStorage->Prefix = 'trtc-recording/'; + +$storage = new \TencentCloud\Trtc\V20190722\Models\StorageParams(); +$storage->CloudStorage = $cloudStorage; +``` + +#### 2. 更新日志信息 +- 日志中标注存储类型为 COS +- 记录 bucket、region、prefix 等信息 +- 更新提示信息 + +#### 3. 更新路径清理函数 +- 允许路径中包含 `/` 字符(COS 路径分隔符) +- 从 `sanitizeVodUserDefineRecordId` 改为支持 COS 路径格式 + +## 配置要求 + +### 必需配置项 + +在 `config/trtc.php` 或 `.env` 中添加以下配置: + +```php +// config/trtc.php +return [ + // ... 其他配置 ... + + // COS 存储配置 + 'recording_cos_vendor' => 0, // 0=腾讯云 + 'recording_cos_region' => env('TRTC_RECORDING_COS_REGION', 'ap-guangzhou'), + 'recording_cos_bucket' => env('TRTC_RECORDING_COS_BUCKET', ''), + 'recording_cos_prefix' => env('TRTC_RECORDING_COS_PREFIX', 'trtc-recording/'), +]; +``` + +### .env 配置示例 + +```env +# TRTC 录制 COS 存储配置 +TRTC_RECORDING_COS_REGION=ap-guangzhou +TRTC_RECORDING_COS_BUCKET=your-bucket-1234567890 +TRTC_RECORDING_COS_PREFIX=trtc-recording/ +``` + +## COS Bucket 准备 + +### 1. 创建 COS 存储桶 + +1. 登录 [腾讯云 COS 控制台](https://console.cloud.tencent.com/cos) +2. 创建存储桶 + - 名称:例如 `trtc-recording-1234567890` + - 地域:选择与 TRTC 应用相同或就近的地域 + - 访问权限:私有读写(推荐) + +### 2. 配置 CORS(如需前端访问) + +如果需要前端直接访问录制文件,配置 CORS 规则: + +```json +{ + "CORSRules": [ + { + "AllowedOrigins": ["*"], + "AllowedMethods": ["GET", "HEAD"], + "AllowedHeaders": ["*"], + "ExposeHeaders": [], + "MaxAgeSeconds": 3600 + } + ] +} +``` + +### 3. 配置生命周期(可选) + +设置自动删除过期录制文件: + +- 规则名称:`auto-delete-old-recordings` +- 应用范围:前缀 `trtc-recording/` +- 删除策略:创建后 30 天删除 + +## 存储路径结构 + +### 默认路径格式 + +``` +bucket-name/ +└── trtc-recording/ + ├── room_123_20240101_120000.mp4 + ├── room_456_20240101_130000.mp4 + └── ... +``` + +### 自定义前缀路径 + +如果传入 `$vodUserDefineRecordId` 参数: + +``` +bucket-name/ +└── custom-prefix/ + ├── file1.mp4 + ├── file2.mp4 + └── ... +``` + +## 文件命名规则 + +COS 存储的文件名由 TRTC 自动生成,通常包含: +- 房间号 +- 时间戳 +- 用户 ID(单流录制) +- 文件格式(.mp4) + +示例: +``` +trtc-recording/sdkappid_roomid_userid_timestamp.mp4 +``` + +## 权限配置 + +### TRTC 服务需要的 COS 权限 + +确保 TRTC 服务有权限写入 COS: + +1. **方式一:使用主账号密钥**(不推荐生产环境) + - 使用主账号的 SecretId 和 SecretKey + +2. **方式二:使用子账号密钥**(推荐) + - 创建子账号 + - 授予 COS 写入权限策略: + ```json + { + "version": "2.0", + "statement": [ + { + "effect": "allow", + "action": [ + "cos:PutObject", + "cos:PostObject", + "cos:InitiateMultipartUpload", + "cos:UploadPart", + "cos:CompleteMultipartUpload" + ], + "resource": [ + "qcs::cos:ap-guangzhou:uid/1234567890:your-bucket-1234567890/*" + ] + } + ] + } + ``` + +3. **方式三:使用服务角色**(最佳实践) + - 为 TRTC 服务创建角色 + - 授予角色 COS 写入权限 + - TRTC 自动使用角色权限 + +## 对比:VOD vs COS + +| 特性 | VOD(点播) | COS(对象存储) | +|------|------------|----------------| +| **存储成本** | 较高 | 较低 | +| **功能** | 媒体处理、转码、播放 | 纯存储 | +| **访问方式** | 点播 API/播放器 | HTTP/HTTPS 直接访问 | +| **适用场景** | 需要转码、加密、播放统计 | 简单存储和下载 | +| **计费** | 存储+流量+转码 | 存储+流量 | +| **管理** | 媒资管理系统 | 文件管理 | + +## 迁移步骤 + +### 1. 准备 COS 存储桶 +按照上述"COS Bucket 准备"步骤创建并配置存储桶 + +### 2. 更新配置文件 +在 `.env` 中添加 COS 配置项 + +### 3. 部署代码 +部署更新后的 `TrtcCloudRecordingService.php` + +### 4. 测试录制 +1. 发起一次测试录制 +2. 检查 COS 控制台是否有文件上传 +3. 验证文件可以正常下载和播放 + +### 5. 监控日志 +查看日志确认录制状态: +``` +CreateCloudRecording mix (COS) +DescribeCloudRecording(合流校验-COS) +``` + +## 获取录制文件 + +### 方式一:COS 控制台 +1. 登录 COS 控制台 +2. 进入对应存储桶 +3. 浏览 `trtc-recording/` 目录 +4. 下载或获取文件 URL + +### 方式二:COS SDK +```php +use Qcloud\Cos\Client; + +$cosClient = new Client([ + 'region' => 'ap-guangzhou', + 'credentials' => [ + 'secretId' => 'your-secret-id', + 'secretKey' => 'your-secret-key', + ], +]); + +// 列出录制文件 +$result = $cosClient->listObjects([ + 'Bucket' => 'your-bucket-1234567890', + 'Prefix' => 'trtc-recording/', +]); + +foreach ($result['Contents'] as $file) { + echo $file['Key'] . "\n"; +} +``` + +### 方式三:生成临时访问 URL +```php +$url = $cosClient->getObjectUrl( + 'your-bucket-1234567890', + 'trtc-recording/file.mp4', + '+10 minutes' // URL 有效期 +); +``` + +## 回调通知 + +### COS 事件通知 +可以配置 COS 事件通知,在文件上传完成时触发回调: + +1. 在 COS 控制台配置事件通知 +2. 选择事件类型:`cos:ObjectCreated:*` +3. 配置回调 URL +4. 接收通知并处理 + +### 回调数据示例 +```json +{ + "Records": [ + { + "event": { + "eventName": "cos:ObjectCreated:Put", + "eventTime": "2024-01-01T12:00:00Z" + }, + "cos": { + "cosObject": { + "key": "trtc-recording/room_123_20240101_120000.mp4", + "size": 12345678, + "url": "https://bucket.cos.ap-guangzhou.myqcloud.com/..." + } + } + } + ] +} +``` + +## 故障排查 + +### 问题1:录制文件未上传到 COS +**检查项:** +- COS Bucket 名称是否正确 +- Region 是否匹配 +- TRTC 服务是否有 COS 写入权限 +- 查看日志中的错误信息 + +### 问题2:无法访问录制文件 +**检查项:** +- Bucket 访问权限设置 +- 文件路径是否正确 +- 是否需要签名 URL + +### 问题3:录制状态一直是 Idle +**检查项:** +- RoomIdType 是否与客户端一致 +- 房间内是否有用户推流 +- 网络连接是否正常 + +## 成本估算 + +### COS 存储成本(以广州地域为例) + +**存储费用:** +- 标准存储:0.118 元/GB/月 +- 低频存储:0.08 元/GB/月 + +**流量费用:** +- 外网下行流量:0.5 元/GB +- CDN 回源流量:0.15 元/GB + +**示例计算:** +- 每天录制 10 小时,每小时 500MB +- 月存储量:10 × 30 × 0.5 = 150GB +- 月存储费用:150 × 0.118 = 17.7 元 +- 月流量(假设下载 50GB):50 × 0.5 = 25 元 +- **月总费用:约 42.7 元** + +## 相关文档 + +- [TRTC 云端录制](https://cloud.tencent.com/document/product/647/16823) +- [COS 对象存储](https://cloud.tencent.com/document/product/436) +- [COS PHP SDK](https://cloud.tencent.com/document/product/436/12266) + +## 总结 + +✅ 存储方式从 VOD 改为 COS +✅ 降低存储成本 +✅ 简化文件管理 +✅ 支持自定义路径前缀 +✅ 更新日志和提示信息 +✅ 需要配置 COS Bucket 和权限 + +迁移到 COS 后,录制文件将直接存储到对象存储桶,成本更低,管理更简单! diff --git a/admin/src/views/consumer/prescription/index.vue b/admin/src/views/consumer/prescription/index.vue index 78bb048b..ccdb79f5 100644 --- a/admin/src/views/consumer/prescription/index.vue +++ b/admin/src/views/consumer/prescription/index.vue @@ -951,7 +951,7 @@
-
+
@@ -1072,7 +1072,7 @@ - + { const loadRegionData = async () => { try { console.log('开始加载省市区数据...') - // 使用代理路径,需要在vite.config.ts中配置代理 - const response = await fetch('/api-proxy/area/ChinaCitys.json') + // 开发环境使用代理,生产环境使用环境变量中的基础URL + const apiUrl = import.meta.env.DEV + ? '/api-proxy/area/ChinaCitys.json' + : `https://cos.zhenyangtang.com.cn/area/ChinaCitys.json` + + const response = await fetch(apiUrl) console.log('响应状态:', response.status) if (!response.ok) { diff --git a/admin/src/views/consumer/prescription/order_list.vue b/admin/src/views/consumer/prescription/order_list.vue index c648987d..8eb2b854 100644 --- a/admin/src/views/consumer/prescription/order_list.vue +++ b/admin/src/views/consumer/prescription/order_list.vue @@ -8,6 +8,35 @@
+ + +
+
+
+ {{ tab.label }} + + {{ tab.count }} + +
+
+
+
+ @@ -26,15 +55,6 @@ @keyup.enter="resetPage" /> - - - - - - - - - 查询 重置 @@ -51,8 +71,6 @@ - - + + +