1. 优化了一些交互,中药药品编辑交互
2. dev和生产环境统一鉴别
This commit is contained in:
152
api/clinicAdmin.js
Normal file
152
api/clinicAdmin.js
Normal file
@@ -0,0 +1,152 @@
|
||||
import { req } from '@/common/js/index.js';
|
||||
|
||||
const PREFIX = '/newApi/clinic-admin';
|
||||
|
||||
/** 解析 Laravel jok 或 Yii 响应 */
|
||||
export function unwrapClinicRes(res) {
|
||||
if (!res) return null;
|
||||
if (res.code === 0 && res.result !== undefined) return res.result;
|
||||
if (res.errcode != null && res.errcode !== -1) return res.data;
|
||||
if (res.result !== undefined) return res.result;
|
||||
return res.data != null ? res.data : res;
|
||||
}
|
||||
|
||||
function clinicRequest(options) {
|
||||
return req.request({
|
||||
...options,
|
||||
header: {
|
||||
...(options.header || {}),
|
||||
_clinicAdmin: '1',
|
||||
},
|
||||
}).then((res) => {
|
||||
const data = unwrapClinicRes(res);
|
||||
const ok = res && (res.code === 0 || (res.errcode != null && Number(res.errcode) !== -1));
|
||||
return { res, data, ok };
|
||||
});
|
||||
}
|
||||
|
||||
export function sendClinicAdminCode(phone) {
|
||||
return req.request({
|
||||
url: '/newApi/clinic-admin-auth/send-verification-code',
|
||||
method: 'POST',
|
||||
data: { account: phone },
|
||||
header: { isToken: false, _clinicAdmin: '1' },
|
||||
});
|
||||
}
|
||||
|
||||
export function clinicAdminLogin(phone, password, code) {
|
||||
return req.request({
|
||||
url: '/newApi/clinic-admin-auth/login',
|
||||
method: 'POST',
|
||||
data: { account: phone, password, code },
|
||||
header: { isToken: false, _clinicAdmin: '1' },
|
||||
});
|
||||
}
|
||||
|
||||
export function getClinicAdminMyInfo() {
|
||||
return clinicRequest({ url: `${PREFIX}-auth/my-info`, method: 'GET' });
|
||||
}
|
||||
|
||||
export function getClinicAdminBalance() {
|
||||
return clinicRequest({ url: `${PREFIX}-account/my-balance`, method: 'GET' });
|
||||
}
|
||||
|
||||
export function getClinicAdminCard() {
|
||||
return clinicRequest({ url: `${PREFIX}-account/my-card`, method: 'GET' });
|
||||
}
|
||||
|
||||
export function saveClinicAdminCard(data) {
|
||||
return clinicRequest({ url: `${PREFIX}-account/save-card`, method: 'POST', data });
|
||||
}
|
||||
|
||||
export function getLedgerLogList(params) {
|
||||
return clinicRequest({ url: `${PREFIX}-settlement/ledger-log-list`, method: 'GET', data: params });
|
||||
}
|
||||
|
||||
export function getLedgerDetail(params) {
|
||||
return clinicRequest({ url: `${PREFIX}-settlement/ledger-detail`, method: 'GET', data: params });
|
||||
}
|
||||
|
||||
export function getWithdrawalList(params) {
|
||||
return clinicRequest({ url: `${PREFIX}-withdrawal/list`, method: 'GET', data: params });
|
||||
}
|
||||
|
||||
export function getWithdrawalDetail(id) {
|
||||
return clinicRequest({ url: `${PREFIX}-withdrawal/detail`, method: 'GET', data: { id } });
|
||||
}
|
||||
|
||||
export function submitWithdrawal(data) {
|
||||
return clinicRequest({ url: `${PREFIX}-withdrawal/withdrawal`, method: 'POST', data });
|
||||
}
|
||||
|
||||
export function getReconciliationList(params) {
|
||||
return clinicRequest({ url: `${PREFIX}-reconciliation/list`, method: 'GET', data: params });
|
||||
}
|
||||
|
||||
export function getReconciliationDrugList(params) {
|
||||
return clinicRequest({ url: `${PREFIX}-reconciliation/drug-list`, method: 'GET', data: params });
|
||||
}
|
||||
|
||||
export function getReconciliationDrugOptions(params) {
|
||||
return clinicRequest({ url: `${PREFIX}-reconciliation/drug-options`, method: 'GET', data: params });
|
||||
}
|
||||
|
||||
export function getReconciliationOrderOptions(params) {
|
||||
return clinicRequest({ url: `${PREFIX}-reconciliation/order-options`, method: 'GET', data: params });
|
||||
}
|
||||
|
||||
export function getOrderSaleAmount(params) {
|
||||
return clinicRequest({ url: `${PREFIX}-order/sale-amount`, method: 'GET', data: params });
|
||||
}
|
||||
|
||||
export function getPrescriptionDetail(id) {
|
||||
return clinicRequest({ url: `${PREFIX}-prescription/detail`, method: 'GET', data: { id } });
|
||||
}
|
||||
|
||||
export function getOrderList(params) {
|
||||
return clinicRequest({ url: `${PREFIX}-order/list`, method: 'GET', data: params });
|
||||
}
|
||||
|
||||
export function getOrderStatusOption() {
|
||||
return clinicRequest({ url: `${PREFIX}-order/status-option`, method: 'GET' });
|
||||
}
|
||||
|
||||
export function getOrderDetail(id) {
|
||||
return clinicRequest({ url: `${PREFIX}-order/detail`, method: 'GET', data: { id } });
|
||||
}
|
||||
|
||||
export function refundOrder(data) {
|
||||
return clinicRequest({ url: `${PREFIX}-order/refund`, method: 'POST', data });
|
||||
}
|
||||
|
||||
export function wareSendOrder(data) {
|
||||
return clinicRequest({ url: `${PREFIX}-order/ware-send`, method: 'POST', data });
|
||||
}
|
||||
|
||||
export function getExpressDetailByOrderId(orderId) {
|
||||
return clinicRequest({
|
||||
url: `${PREFIX}-express-detail/detail-by-order`,
|
||||
method: 'POST',
|
||||
data: { order_id: orderId },
|
||||
});
|
||||
}
|
||||
|
||||
export function getWarehouseList(params) {
|
||||
return clinicRequest({ url: `${PREFIX}-warehouse/list`, method: 'GET', data: params });
|
||||
}
|
||||
|
||||
export function getWarehouseDetail(id) {
|
||||
return clinicRequest({ url: `${PREFIX}-warehouse/detail`, method: 'GET', data: { id } });
|
||||
}
|
||||
|
||||
export function updateWarehouseStatus(id) {
|
||||
return clinicRequest({ url: `${PREFIX}-warehouse/update-status`, method: 'GET', data: { id } });
|
||||
}
|
||||
|
||||
export function updateWarehousePrice(data) {
|
||||
return clinicRequest({ url: `${PREFIX}-warehouse/update`, method: 'POST', data });
|
||||
}
|
||||
|
||||
export function getWarehouseProductTypeOptions() {
|
||||
return clinicRequest({ url: `${PREFIX}-warehouse/product-type-options`, method: 'GET' });
|
||||
}
|
||||
23
common/js/image-compress-modal.js
Normal file
23
common/js/image-compress-modal.js
Normal file
@@ -0,0 +1,23 @@
|
||||
let popupInstance = null;
|
||||
|
||||
export function registerImageCompressPopup(instance) {
|
||||
popupInstance = instance;
|
||||
}
|
||||
|
||||
export function openImageCompressModal({ path, size }) {
|
||||
if (!popupInstance || typeof popupInstance.open !== 'function') {
|
||||
console.warn('image-compress-popup 未注册,将直接使用原图');
|
||||
return Promise.resolve({
|
||||
path,
|
||||
size,
|
||||
usedCompress: false,
|
||||
});
|
||||
}
|
||||
|
||||
return popupInstance.open({ path, size }).then((result) => {
|
||||
if (!result) {
|
||||
return null;
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
95
common/js/image-compress.js
Normal file
95
common/js/image-compress.js
Normal file
@@ -0,0 +1,95 @@
|
||||
import { openImageCompressModal } from '@/common/js/image-compress-modal.js';
|
||||
|
||||
export const IMAGE_COMPRESS_THRESHOLD = 1 * 1024 * 1024;
|
||||
|
||||
export function formatFileSize(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
const value = bytes / k ** i;
|
||||
return `${Number.parseFloat(value.toFixed(2))} ${sizes[i]}`;
|
||||
}
|
||||
|
||||
/** 压缩节省比例文案,如「(约节省 42%)」 */
|
||||
export function formatSavingsPercent(originalBytes, compressedBytes) {
|
||||
if (originalBytes <= 0 || compressedBytes >= originalBytes) {
|
||||
return '';
|
||||
}
|
||||
const percent = Math.round(
|
||||
((originalBytes - compressedBytes) / originalBytes) * 100,
|
||||
);
|
||||
return percent > 0 ? `(约节省 ${percent}%)` : '';
|
||||
}
|
||||
|
||||
function getFileInfo(filePath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.getFileInfo({
|
||||
filePath,
|
||||
success: resolve,
|
||||
fail: reject,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function compressImagePath(src, quality) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.compressImage({
|
||||
src,
|
||||
quality,
|
||||
success: async (res) => {
|
||||
try {
|
||||
const info = await getFileInfo(res.tempFilePath);
|
||||
resolve({
|
||||
path: res.tempFilePath,
|
||||
size: info.size,
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
},
|
||||
fail: reject,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function tryCompressImage(
|
||||
src,
|
||||
qualities = [80, 60, 40],
|
||||
onProgress,
|
||||
) {
|
||||
let result = null;
|
||||
for (const quality of qualities) {
|
||||
onProgress && onProgress(quality);
|
||||
result = await compressImagePath(src, quality);
|
||||
if (result.size <= IMAGE_COMPRESS_THRESHOLD) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function isGifPath(path) {
|
||||
return /\.gif$/i.test(path || '');
|
||||
}
|
||||
|
||||
export async function prepareImagePath({ path, size }) {
|
||||
if (size <= IMAGE_COMPRESS_THRESHOLD) {
|
||||
return {
|
||||
path,
|
||||
size,
|
||||
usedCompress: false,
|
||||
};
|
||||
}
|
||||
|
||||
const result = await openImageCompressModal({ path, size });
|
||||
if (!result) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
path: result.path,
|
||||
size: result.size,
|
||||
usedCompress: !!result.usedCompress,
|
||||
};
|
||||
}
|
||||
60
common/js/select-avatar-image.js
Normal file
60
common/js/select-avatar-image.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import { prepareImagePath } from '@/common/js/image-compress.js';
|
||||
|
||||
export function chooseAvatarImage(options = {}) {
|
||||
const {
|
||||
uploadUrl = 'https://api.xiaokang88.com/open-api/upload/old-admin-img',
|
||||
formData = { store_id: '11001' },
|
||||
onSuccess,
|
||||
onFail,
|
||||
onCancel,
|
||||
} = options;
|
||||
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
sizeType: ['original', 'compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: async (res) => {
|
||||
uni.showLoading({ title: '请稍后...' });
|
||||
|
||||
try {
|
||||
const tempFile = res.tempFiles[0];
|
||||
const prepared = await prepareImagePath({
|
||||
path: tempFile.path,
|
||||
size: tempFile.size,
|
||||
});
|
||||
|
||||
if (!prepared) {
|
||||
uni.hideLoading();
|
||||
onCancel && onCancel();
|
||||
return;
|
||||
}
|
||||
|
||||
uni.uploadFile({
|
||||
url: uploadUrl,
|
||||
filePath: prepared.path,
|
||||
name: 'file',
|
||||
header: {
|
||||
authorization: `Bearer ${uni.getStorageSync('token')}`,
|
||||
},
|
||||
formData,
|
||||
success: (uploadRes) => {
|
||||
uni.hideLoading();
|
||||
onSuccess && onSuccess(uploadRes);
|
||||
},
|
||||
fail: (error) => {
|
||||
uni.hideLoading();
|
||||
onFail && onFail(error);
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
uni.hideLoading();
|
||||
onFail && onFail(error);
|
||||
}
|
||||
},
|
||||
complete: (res) => {
|
||||
if (res.errMsg === 'chooseImage:fail cancel') {
|
||||
onCancel && onCancel();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
676
components/image-compress-popup/image-compress-popup.vue
Normal file
676
components/image-compress-popup/image-compress-popup.vue
Normal file
@@ -0,0 +1,676 @@
|
||||
<template>
|
||||
|
||||
<u-popup v-model="visible" mode="center" width="85%" border-radius="16" :mask-close-able="false">
|
||||
|
||||
<view class="popup-wrap">
|
||||
|
||||
<view class="popup-title">图片较大</view>
|
||||
|
||||
<view v-if="sizeText" class="size-text">{{ sizeText }}</view>
|
||||
|
||||
|
||||
|
||||
<view v-if="status === 'compressing'" class="compressing-panel">
|
||||
|
||||
<image
|
||||
|
||||
v-if="originalPath"
|
||||
|
||||
:src="originalPath"
|
||||
|
||||
mode="aspectFit"
|
||||
|
||||
class="preview-image single"
|
||||
|
||||
@click="previewImage(originalPath)"
|
||||
|
||||
/>
|
||||
|
||||
<text v-if="originalPath" class="preview-hint">点击预览</text>
|
||||
|
||||
<view class="compressing-status">
|
||||
|
||||
<u-loading mode="circle" />
|
||||
|
||||
<text class="progress-text">{{ progressText }}</text>
|
||||
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
|
||||
|
||||
<view v-else-if="status === 'ready'" class="compare-panel">
|
||||
|
||||
<view class="preview-column">
|
||||
|
||||
<text class="preview-label">原图</text>
|
||||
|
||||
<image
|
||||
|
||||
:src="originalPath"
|
||||
|
||||
mode="aspectFit"
|
||||
|
||||
class="preview-image"
|
||||
|
||||
@click="previewImage(originalPath)"
|
||||
|
||||
/>
|
||||
|
||||
<text class="preview-size">{{ formatFileSize(originalSize) }}</text>
|
||||
|
||||
<text class="preview-hint">点击预览</text>
|
||||
|
||||
</view>
|
||||
|
||||
<view class="preview-column">
|
||||
|
||||
<text class="preview-label">压缩后</text>
|
||||
|
||||
<image
|
||||
|
||||
:src="compressedPath"
|
||||
|
||||
mode="aspectFit"
|
||||
|
||||
class="preview-image"
|
||||
|
||||
@click="previewImage(compressedPath)"
|
||||
|
||||
/>
|
||||
|
||||
<text class="preview-size">{{ formatFileSize(compressedSize) }}</text>
|
||||
|
||||
<text class="preview-hint">点击预览</text>
|
||||
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
|
||||
|
||||
<view v-else-if="status === 'gifSkipped'" class="gif-panel">
|
||||
|
||||
<image
|
||||
|
||||
v-if="originalPath"
|
||||
|
||||
:src="originalPath"
|
||||
|
||||
mode="aspectFit"
|
||||
|
||||
class="preview-image single"
|
||||
|
||||
@click="previewImage(originalPath)"
|
||||
|
||||
/>
|
||||
|
||||
<text v-if="originalPath" class="preview-hint">点击预览</text>
|
||||
|
||||
<text class="hint-text">GIF 不支持压缩,将上传原图。</text>
|
||||
|
||||
</view>
|
||||
|
||||
|
||||
|
||||
<view v-else class="error-panel">
|
||||
|
||||
<image
|
||||
|
||||
v-if="originalPath"
|
||||
|
||||
:src="originalPath"
|
||||
|
||||
mode="aspectFit"
|
||||
|
||||
class="preview-image single"
|
||||
|
||||
@click="previewImage(originalPath)"
|
||||
|
||||
/>
|
||||
|
||||
<text v-if="originalPath" class="preview-hint">点击预览</text>
|
||||
|
||||
<text class="error-text">{{ errorMessage }}</text>
|
||||
|
||||
</view>
|
||||
|
||||
|
||||
|
||||
<view class="footer-actions">
|
||||
|
||||
<view class="btn btn-default" @click="finish('cancel')">取消</view>
|
||||
|
||||
<view
|
||||
|
||||
v-if="status === 'ready' || status === 'error'"
|
||||
|
||||
class="btn btn-default"
|
||||
|
||||
@click="finish('original')"
|
||||
|
||||
>
|
||||
|
||||
{{ originalButtonLabel }}
|
||||
|
||||
</view>
|
||||
|
||||
<view
|
||||
|
||||
v-if="status === 'ready'"
|
||||
|
||||
class="btn btn-primary"
|
||||
|
||||
@click="finish('compressed')"
|
||||
|
||||
>
|
||||
|
||||
{{ compressedButtonLabel }}
|
||||
|
||||
</view>
|
||||
|
||||
<view
|
||||
|
||||
v-if="status === 'gifSkipped'"
|
||||
|
||||
class="btn btn-primary"
|
||||
|
||||
@click="finish('original')"
|
||||
|
||||
>
|
||||
|
||||
{{ originalButtonLabel }}
|
||||
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
</u-popup>
|
||||
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
import {
|
||||
|
||||
formatFileSize,
|
||||
|
||||
formatSavingsPercent,
|
||||
|
||||
isGifPath,
|
||||
|
||||
tryCompressImage,
|
||||
|
||||
} from '@/common/js/image-compress.js';
|
||||
|
||||
|
||||
|
||||
export default {
|
||||
|
||||
name: 'ImageCompressPopup',
|
||||
|
||||
data() {
|
||||
|
||||
return {
|
||||
|
||||
visible: false,
|
||||
|
||||
status: 'compressing',
|
||||
|
||||
progressText: '正在压缩,请稍候…',
|
||||
|
||||
errorMessage: '',
|
||||
|
||||
originalPath: '',
|
||||
|
||||
compressedPath: '',
|
||||
|
||||
originalSize: 0,
|
||||
|
||||
compressedSize: 0,
|
||||
|
||||
skipped: false,
|
||||
|
||||
resolver: null,
|
||||
|
||||
};
|
||||
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
sizeText() {
|
||||
|
||||
if (!this.originalSize) return '';
|
||||
|
||||
const originalText = formatFileSize(this.originalSize);
|
||||
|
||||
if (this.status === 'gifSkipped') {
|
||||
|
||||
return `原图 ${originalText}`;
|
||||
|
||||
}
|
||||
|
||||
if (this.compressedSize && this.status === 'ready') {
|
||||
|
||||
const savings = formatSavingsPercent(this.originalSize, this.compressedSize);
|
||||
|
||||
return `原图 ${originalText} → 压缩后 ${formatFileSize(this.compressedSize)}${savings}`;
|
||||
|
||||
}
|
||||
|
||||
if (this.compressedSize) {
|
||||
|
||||
return `原图 ${originalText} → 压缩后 ${formatFileSize(this.compressedSize)}`;
|
||||
|
||||
}
|
||||
|
||||
return `原图 ${originalText}`;
|
||||
|
||||
},
|
||||
|
||||
originalButtonLabel() {
|
||||
|
||||
if (!this.originalSize) return '使用原图';
|
||||
|
||||
return `使用原图 (${formatFileSize(this.originalSize)})`;
|
||||
|
||||
},
|
||||
|
||||
compressedButtonLabel() {
|
||||
|
||||
if (!this.compressedSize) return '使用压缩图';
|
||||
|
||||
return `使用压缩图 (${formatFileSize(this.compressedSize)})`;
|
||||
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
formatFileSize,
|
||||
|
||||
previewImage(current) {
|
||||
|
||||
if (!this.originalPath) return;
|
||||
|
||||
const urls =
|
||||
|
||||
this.status === 'ready' && this.compressedPath
|
||||
|
||||
? [this.originalPath, this.compressedPath]
|
||||
|
||||
: [this.originalPath];
|
||||
|
||||
uni.previewImage({
|
||||
|
||||
urls,
|
||||
|
||||
current,
|
||||
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
open({ path, size }) {
|
||||
|
||||
return new Promise((resolve) => {
|
||||
|
||||
this.resolver = resolve;
|
||||
|
||||
this.originalPath = path;
|
||||
|
||||
this.originalSize = size;
|
||||
|
||||
this.compressedPath = '';
|
||||
|
||||
this.compressedSize = 0;
|
||||
|
||||
this.skipped = false;
|
||||
|
||||
this.errorMessage = '';
|
||||
|
||||
this.progressText = '正在压缩,请稍候…';
|
||||
|
||||
this.visible = true;
|
||||
|
||||
this.startCompress(path, size);
|
||||
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
async startCompress(path, size) {
|
||||
|
||||
if (isGifPath(path)) {
|
||||
|
||||
this.skipped = true;
|
||||
|
||||
this.status = 'gifSkipped';
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
this.status = 'compressing';
|
||||
|
||||
this.progressText = '正在压缩(质量 80%)…';
|
||||
|
||||
|
||||
|
||||
try {
|
||||
|
||||
const compressed = await tryCompressImage(path, [80, 60, 40], (quality) => {
|
||||
|
||||
this.progressText = `正在压缩(质量 ${quality}%)…`;
|
||||
|
||||
});
|
||||
|
||||
this.compressedPath = compressed.path;
|
||||
|
||||
this.compressedSize = compressed.size;
|
||||
|
||||
this.status = 'ready';
|
||||
|
||||
this.progressText = '';
|
||||
|
||||
} catch (error) {
|
||||
|
||||
this.status = 'error';
|
||||
|
||||
this.errorMessage = '图片压缩失败,请使用原图或取消';
|
||||
|
||||
this.progressText = '';
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
finish(choice) {
|
||||
|
||||
const result = { choice, path: null, size: 0, usedCompress: false };
|
||||
|
||||
|
||||
|
||||
if (choice === 'cancel') {
|
||||
|
||||
this.visible = false;
|
||||
|
||||
this.resolver && this.resolver(null);
|
||||
|
||||
this.resolver = null;
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (choice === 'compressed' && this.status === 'ready') {
|
||||
|
||||
result.path = this.compressedPath;
|
||||
|
||||
result.size = this.compressedSize;
|
||||
|
||||
result.usedCompress = true;
|
||||
|
||||
} else {
|
||||
|
||||
result.path = this.originalPath;
|
||||
|
||||
result.size = this.originalSize;
|
||||
|
||||
result.usedCompress = false;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
this.visible = false;
|
||||
|
||||
this.resolver && this.resolver(result);
|
||||
|
||||
this.resolver = null;
|
||||
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.popup-wrap {
|
||||
|
||||
padding: 32rpx;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.popup-title {
|
||||
|
||||
font-size: 32rpx;
|
||||
|
||||
font-weight: 600;
|
||||
|
||||
text-align: center;
|
||||
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.size-text {
|
||||
|
||||
font-size: 26rpx;
|
||||
|
||||
color: #666;
|
||||
|
||||
text-align: center;
|
||||
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.preview-image {
|
||||
|
||||
width: 100%;
|
||||
|
||||
height: 280rpx;
|
||||
|
||||
background: #fafafa;
|
||||
|
||||
border-radius: 12rpx;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.preview-image.single {
|
||||
|
||||
width: 100%;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.compare-panel {
|
||||
|
||||
display: flex;
|
||||
|
||||
gap: 16rpx;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.preview-column {
|
||||
|
||||
flex: 1;
|
||||
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
|
||||
align-items: center;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.preview-label {
|
||||
|
||||
font-size: 26rpx;
|
||||
|
||||
font-weight: 500;
|
||||
|
||||
margin-bottom: 12rpx;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.preview-size {
|
||||
|
||||
margin-top: 8rpx;
|
||||
|
||||
font-size: 22rpx;
|
||||
|
||||
color: #999;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.preview-hint {
|
||||
|
||||
margin-top: 4rpx;
|
||||
|
||||
font-size: 22rpx;
|
||||
|
||||
color: #bbb;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.compressing-panel,
|
||||
|
||||
.gif-panel,
|
||||
|
||||
.error-panel {
|
||||
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
|
||||
align-items: center;
|
||||
|
||||
gap: 24rpx;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.compressing-status {
|
||||
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
gap: 16rpx;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.progress-text,
|
||||
|
||||
.hint-text {
|
||||
|
||||
font-size: 26rpx;
|
||||
|
||||
color: #666;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.error-text {
|
||||
|
||||
font-size: 26rpx;
|
||||
|
||||
color: #fa3534;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.footer-actions {
|
||||
|
||||
display: flex;
|
||||
|
||||
justify-content: flex-end;
|
||||
|
||||
flex-wrap: wrap;
|
||||
|
||||
gap: 16rpx;
|
||||
|
||||
margin-top: 32rpx;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.btn {
|
||||
|
||||
min-width: 160rpx;
|
||||
|
||||
padding: 16rpx 24rpx;
|
||||
|
||||
border-radius: 8rpx;
|
||||
|
||||
font-size: 26rpx;
|
||||
|
||||
text-align: center;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.btn-default {
|
||||
|
||||
background: #f5f5f5;
|
||||
|
||||
color: #333;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.btn-primary {
|
||||
|
||||
background: #2979ff;
|
||||
|
||||
color: #fff;
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
513
subPackages/sub_clinic_admin/common/display.js
Normal file
513
subPackages/sub_clinic_admin/common/display.js
Normal file
@@ -0,0 +1,513 @@
|
||||
import { formatMoney } from './format.js'
|
||||
|
||||
const DEFAULT_DRUG_IMAGE =
|
||||
'https://xkyp-web2.obs.cn-south-1.myhuaweicloud.com/buy/public/drug_no_mage.png'
|
||||
|
||||
/** 兼容 int 时间戳 / datetime 字符串 */
|
||||
export function formatDateTime(v) {
|
||||
if (v === null || v === undefined || v === '') return '-'
|
||||
if (typeof v === 'number') {
|
||||
const ms = v < 1e12 ? v * 1000 : v
|
||||
const d = new Date(ms)
|
||||
if (Number.isNaN(d.getTime())) return '-'
|
||||
return d.toLocaleString()
|
||||
}
|
||||
const s = String(v)
|
||||
if (s === '2021-01-01 00:00:00') return '-'
|
||||
return s
|
||||
}
|
||||
|
||||
export function withdrawalCheckStatusText(s) {
|
||||
const m = { 1: '待审核', 2: '审核成功', 3: '拒绝', 4: '提现失败' }
|
||||
return m[s] || '-'
|
||||
}
|
||||
|
||||
export function dakuanStatusText(s) {
|
||||
if (s === -1) return '打款失败'
|
||||
if (s === 0) return '处理中'
|
||||
if (s === 1) return '打款成功'
|
||||
return '-'
|
||||
}
|
||||
|
||||
export function isWithdrawalRejected(item) {
|
||||
return item && Number(item.check_status) === 3
|
||||
}
|
||||
|
||||
/** 提现列表行展示(yii_cash_apply) */
|
||||
export function mapWithdrawalRow(item) {
|
||||
if (!item) return {}
|
||||
const checkStatus = item.check_status
|
||||
return {
|
||||
id: item.id,
|
||||
orderNo: item.order_no || '-',
|
||||
applyCash: formatMoney(item.apply_cash),
|
||||
trueCash: formatMoney(item.true_cash),
|
||||
chargeCash: formatMoney(item.charge_cash),
|
||||
applyTime: formatDateTime(item.apply_time),
|
||||
checkStatusText: item.check_status_txt || withdrawalCheckStatusText(checkStatus),
|
||||
checkResult: item.check_result || '',
|
||||
isRejected: isWithdrawalRejected(item),
|
||||
dakuanStatusText: item.dakuan_status_txt || dakuanStatusText(item.dakuan_status),
|
||||
dakuanTime: formatDateTime(item.dakuan_time),
|
||||
_raw: item,
|
||||
}
|
||||
}
|
||||
|
||||
/** 结算记录列表行(yii_ledger_log) */
|
||||
export function mapSettlementRow(item) {
|
||||
if (!item) return {}
|
||||
return {
|
||||
id: item.id,
|
||||
orderNo: item.order_no || (item.order_id ? '订单#' + item.order_id : '-'),
|
||||
feeTypeText: item.fee_type_txt || '-',
|
||||
amountText: formatMoney(item.amount),
|
||||
amountClass: ledgerTypeClass(item),
|
||||
typeText: item.type_txt || '-',
|
||||
content: item.content || '',
|
||||
createdAt: formatDateTime(item.created_at),
|
||||
_raw: item,
|
||||
}
|
||||
}
|
||||
|
||||
export function ledgerTypeClass(item) {
|
||||
return item && item.type === 1 ? 'inc' : 'dec'
|
||||
}
|
||||
|
||||
/** 商品订单状态(对齐 PC product-order) */
|
||||
export function orderStatusText(status) {
|
||||
const m = {
|
||||
0: '待支付',
|
||||
1: '待发货',
|
||||
2: '待收货',
|
||||
3: '待评价',
|
||||
4: '已退款',
|
||||
5: '退款中',
|
||||
6: '已收货',
|
||||
7: '确认收货',
|
||||
8: '拒绝退款',
|
||||
9: '已取消',
|
||||
}
|
||||
return m[status] != null ? m[status] : '-'
|
||||
}
|
||||
|
||||
export function deliveryMethodText(dm) {
|
||||
if (dm === 0) return '快递到家'
|
||||
if (dm === 1) return '药店自提'
|
||||
return '-'
|
||||
}
|
||||
|
||||
export function orderOnlineText(isOnline) {
|
||||
if (isOnline === 1) return '在线问诊'
|
||||
if (isOnline === 2) return '在线复诊'
|
||||
return '线下就诊'
|
||||
}
|
||||
|
||||
export function prescriptionTypeText(pt) {
|
||||
const m = {
|
||||
1: '中药',
|
||||
2: '西药',
|
||||
3: '保健食品',
|
||||
5: '产品服务包',
|
||||
6: '非药品',
|
||||
7: '医疗器械',
|
||||
}
|
||||
return m[pt] != null ? m[pt] : '其他'
|
||||
}
|
||||
|
||||
export function orderTypeText(ot) {
|
||||
const m = {
|
||||
1: '处方订单',
|
||||
2: '预约购药订单',
|
||||
3: '商城处方订单',
|
||||
}
|
||||
return m[ot] != null ? m[ot] : '其他'
|
||||
}
|
||||
|
||||
export function erpSyncText(v) {
|
||||
if (v === 1) return '已同步'
|
||||
return '未同步'
|
||||
}
|
||||
|
||||
export function freeShippingText(v) {
|
||||
if (v === 1) return '包邮'
|
||||
return '不包邮'
|
||||
}
|
||||
|
||||
function pickReceiverMobile(item) {
|
||||
const addr = item.address
|
||||
if (addr && typeof addr === 'object' && addr.mobile) return addr.mobile
|
||||
return item.patient_mobile || ''
|
||||
}
|
||||
|
||||
function buildProductSummary(items, prescriptionType, max = 2) {
|
||||
if (!Array.isArray(items) || !items.length) return ''
|
||||
return items.slice(0, max).map(p => formatOrderProductLine(p, prescriptionType)).join(';')
|
||||
}
|
||||
|
||||
function pickAddrName(item) {
|
||||
const addr = item.address
|
||||
if (addr && typeof addr === 'object' && addr.name) return addr.name
|
||||
if (typeof addr === 'string') {
|
||||
try {
|
||||
const o = JSON.parse(addr)
|
||||
return o.name || ''
|
||||
} catch (e) {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
return item.patient || item.express_name || '-'
|
||||
}
|
||||
|
||||
/** 订单列表卡片 */
|
||||
export function mapOrderListRow(item) {
|
||||
if (!item) return {}
|
||||
const store = item.store || {}
|
||||
const user = item.user || {}
|
||||
const pt = item.prescription_type
|
||||
return {
|
||||
id: item.id,
|
||||
orderNo: item.order_no || '-',
|
||||
totalPayPrice: formatMoney(item.total_pay_price),
|
||||
itemsPrice: formatMoney(item.items_price),
|
||||
transExpenses: formatMoney(item.trans_expenses),
|
||||
registerPrice: item.register_id != null && item.register_price != null
|
||||
? formatMoney(item.register_price)
|
||||
: '',
|
||||
registerOrderNo: item.register_order_no || '',
|
||||
statusText: orderStatusText(item.status),
|
||||
statusClass: 'tag-status',
|
||||
prescriptionTypeText: prescriptionTypeText(pt),
|
||||
prescriptionTypeClass: 'tag-type',
|
||||
orderTypeText: orderTypeText(item.order_type),
|
||||
onlineText: orderOnlineText(item.is_online),
|
||||
erpStatusText: erpSyncText(item.is_sync_erp),
|
||||
freeShippingText: freeShippingText(item.is_free_shipping),
|
||||
receiverName: pickAddrName(item),
|
||||
receiverMobile: pickReceiverMobile(item),
|
||||
deliveryText: deliveryMethodText(item.delivery_method),
|
||||
deliveryClass: 'tag-delivery',
|
||||
userNickname: user.nickname || '-',
|
||||
payTime: formatDateTime(item.pay_time),
|
||||
createdAt: formatDateTime(item.created_at),
|
||||
storeName: store.name || '-',
|
||||
productSummary: buildProductSummary(item.product_order_items, pt),
|
||||
pId: item.p_id,
|
||||
canViewPrescription: !!(item.p_id && item.order_type !== 2 && item.order_type !== 3),
|
||||
_raw: item,
|
||||
}
|
||||
}
|
||||
|
||||
/** 物流详情(对齐 PC express-detail/detail-by-order) */
|
||||
export function mapExpressDetail(data) {
|
||||
if (!data || typeof data !== 'object') {
|
||||
return { hasData: false, companyName: '', expressNo: '', stateText: '', tracks: [] }
|
||||
}
|
||||
const rawDetail = data.detail
|
||||
const tracks = Array.isArray(rawDetail)
|
||||
? rawDetail.map(d => ({
|
||||
status: d.status || '-',
|
||||
time: d.detail_at || '',
|
||||
detail: d.detail || '',
|
||||
}))
|
||||
: []
|
||||
return {
|
||||
hasData: !!(data.express_no || data.express_company_name || tracks.length),
|
||||
companyName: data.express_company_name || '-',
|
||||
expressNo: data.express_no || '-',
|
||||
stateText: data.state_txt || '-',
|
||||
tracks,
|
||||
}
|
||||
}
|
||||
|
||||
/** 订单详情商品行 */
|
||||
export function mapOrderProductItem(p, prescriptionType) {
|
||||
if (!p) return {}
|
||||
const drug = p.drug || {}
|
||||
const sourceInfo = drug.source_info || {}
|
||||
const manufacturer = sourceInfo.name || drug.source || '暂无'
|
||||
const dosage = p.dosage > 0 ? p.dosage : 1
|
||||
const qty = prescriptionType === 1
|
||||
? '*' + ((p.number != null ? p.number : 0) * dosage)
|
||||
: '×' + (p.number != null ? p.number : 0)
|
||||
const isTcm = prescriptionType === 1
|
||||
return {
|
||||
drugName: p.drug_name || drug.drug_name || '药品',
|
||||
drugNumber: drug.drug_number || '',
|
||||
imageUrl: resolveDrugImage(p.drug_image || drug.image),
|
||||
specification: drug.specification || (isTcm ? 'g' : '暂无'),
|
||||
manufacturer,
|
||||
qty,
|
||||
price: isTcm
|
||||
? (p.price != null && p.price !== '' ? p.price + ' 元/g' : '—')
|
||||
: formatMoney(p.price),
|
||||
buyPrice: p.buy_price != null && p.buy_price !== ''
|
||||
? p.buy_price + ' 元/g'
|
||||
: '—',
|
||||
isTcm,
|
||||
}
|
||||
}
|
||||
|
||||
/** 订单详情处方块 */
|
||||
export function mapOrderPrescription(info) {
|
||||
const pres = info && info.prescription
|
||||
const content = pres && pres.content
|
||||
if (!content || info.order_type === 2 || info.order_type === 3) return null
|
||||
const doctor = content.doctor || {}
|
||||
const depart = doctor.depart || {}
|
||||
const title = doctor.title || {}
|
||||
return {
|
||||
prescriptionNo: content.prescription_no || '-',
|
||||
diagnose: content.clinical_diagnose || '-',
|
||||
doctorOrder: content.doctor_order || '-',
|
||||
doctorName: doctor.name || '-',
|
||||
departName: depart.name || '-',
|
||||
titleName: title.name || '-',
|
||||
}
|
||||
}
|
||||
|
||||
export function mapOrderDetailDisplay(info) {
|
||||
if (!info || !info.id) return {}
|
||||
const freeShip = info.is_free_shipping === 1 || info.free_ship === 1
|
||||
const canView = !!(info.p_id && info.order_type !== 2 && info.order_type !== 3)
|
||||
return {
|
||||
orderTypeText: orderTypeText(info.order_type),
|
||||
marketPrice: formatMoney(info.market_price),
|
||||
freeShippingText: freeShip ? '是' : '否',
|
||||
cancelRemark: info.cancel_remark || '无',
|
||||
showPrescription: mapOrderPrescription(info),
|
||||
prescriptionId: info.p_id,
|
||||
canViewPrescription: canView,
|
||||
statusText: orderStatusText(info.status),
|
||||
prescriptionTypeText: prescriptionTypeText(info.prescription_type),
|
||||
deliveryText: deliveryMethodText(info.delivery_method),
|
||||
}
|
||||
}
|
||||
|
||||
function parseRecipeContent(raw) {
|
||||
if (!raw) return null
|
||||
if (typeof raw === 'object') return raw
|
||||
try {
|
||||
return JSON.parse(raw)
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/** 完整处方详情(对齐 PC PrescriptionDetail.vue) */
|
||||
export function mapPrescriptionDetailView(item) {
|
||||
if (!item) return null
|
||||
const content = item.content || {}
|
||||
const patient = content.patient || {}
|
||||
const doctor = content.doctor || {}
|
||||
const depart = doctor.depart || {}
|
||||
const title = doctor.title || {}
|
||||
const pt = item.prescription_type
|
||||
const recipes = []
|
||||
const repice = content.repice || content.recipe || []
|
||||
const list = Array.isArray(repice) ? repice : []
|
||||
|
||||
if (pt === 1) {
|
||||
list.forEach((recipe) => {
|
||||
const drugs = parseRecipeContent(recipe.content)
|
||||
const drugLines = Array.isArray(drugs)
|
||||
? drugs.map(d => {
|
||||
const name = d.name || d.drug_name || '—'
|
||||
const num = d.number != null ? d.number : ''
|
||||
const way = (d.use_way && d.use_way.name) || d.useWay || '煎服'
|
||||
return { name, qty: num ? num + ' /g' : '', usage: way }
|
||||
})
|
||||
: []
|
||||
recipes.push({
|
||||
type: 'tcm',
|
||||
drugs: drugLines,
|
||||
usage: recipe.consumption != null && recipe.dosage != null
|
||||
? `每日${recipe.consumption}次,共${recipe.dosage}剂`
|
||||
: '',
|
||||
process: [recipe.process_rule_note, recipe.process_rule].filter(Boolean).join(','),
|
||||
})
|
||||
})
|
||||
} else {
|
||||
list.forEach((recipe) => {
|
||||
const d = parseRecipeContent(recipe.content)
|
||||
if (!d) return
|
||||
const unitName = (d.unit && d.unit.name) || d.unit || ''
|
||||
recipes.push({
|
||||
type: 'west',
|
||||
drugs: [{
|
||||
name: d.name || d.drug_name || '—',
|
||||
qty: (d.number != null ? d.number : '') + unitName,
|
||||
usage: d.useWay || (d.use_way && d.use_way.name) || '',
|
||||
}],
|
||||
usage: '',
|
||||
process: '',
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const tcm = item.online_tcm_print || {}
|
||||
return {
|
||||
prescriptionNo: item.prescription_no || content.prescription_no || '-',
|
||||
createdAt: item.created_at || '-',
|
||||
patientName: patient.name || '-',
|
||||
patientSex: patient.sex === 1 ? '男' : patient.sex === 2 ? '女' : '-',
|
||||
patientAge: patient.age != null ? String(patient.age) : '-',
|
||||
category: content.category || '-',
|
||||
diagnose: content.clinical_diagnose || '-',
|
||||
doctorOrder: content.doctor_order || '-',
|
||||
doctorName: doctor.name || '-',
|
||||
departName: depart.name || '-',
|
||||
titleName: title.name || '-',
|
||||
tcmSyndrome: tcm.show && tcm.tcm_syndrome ? tcm.tcm_syndrome : '',
|
||||
tcmMethod: tcm.show && tcm.tcm_method ? tcm.tcm_method : '',
|
||||
tcmDisease: tcm.show && tcm.tcm_disease ? tcm.tcm_disease : '',
|
||||
recipes,
|
||||
prescriptionType: pt,
|
||||
}
|
||||
}
|
||||
|
||||
/** 订单商品行展示 */
|
||||
export function formatOrderProductLine(p, prescriptionType) {
|
||||
if (!p) return '-'
|
||||
const name = p.drug_name || (p.drug && p.drug.drug_name) || '药品'
|
||||
const num = p.number != null ? p.number : ''
|
||||
if (prescriptionType === 1) {
|
||||
const dosage = p.dosage > 0 ? p.dosage : 1
|
||||
return name + ' ×' + (num * dosage)
|
||||
}
|
||||
return name + ' ×' + num
|
||||
}
|
||||
|
||||
export function resolveDrugImage(url) {
|
||||
if (!url) return DEFAULT_DRUG_IMAGE
|
||||
const s = String(url)
|
||||
if (s.indexOf('http://') === 0 || s.indexOf('https://') === 0) return s
|
||||
return DEFAULT_DRUG_IMAGE
|
||||
}
|
||||
|
||||
/** 仓库 1=下架 2=上架 */
|
||||
export function warehouseStatusText(status) {
|
||||
if (status === 2) return '上架'
|
||||
if (status === 1) return '下架'
|
||||
return '-'
|
||||
}
|
||||
|
||||
export function mapWarehouseRow(item) {
|
||||
if (!item) return {}
|
||||
const drug = item.drug || {}
|
||||
const storeDrug = drug.drug_store_drug || {}
|
||||
const suggestPrice = storeDrug.price != null ? storeDrug.price : drug.price
|
||||
return {
|
||||
id: item.id,
|
||||
drugName: drug.drug_name || '-',
|
||||
imageUrl: resolveDrugImage(drug.image),
|
||||
buyPrice: formatMoney(item.buy_price),
|
||||
price: formatMoney(item.price),
|
||||
suggestPrice: formatMoney(suggestPrice),
|
||||
stock: item.stock != null ? item.stock : '-',
|
||||
statusText: warehouseStatusText(item.status),
|
||||
isOnShelf: item.status === 2,
|
||||
pinyin: drug.pinyin || '-',
|
||||
_raw: item,
|
||||
}
|
||||
}
|
||||
|
||||
function mapReconciliationCategory(rec, prefix) {
|
||||
const sales = rec[prefix + '_sales']
|
||||
const salesPrice = rec[prefix + '_sales_price']
|
||||
const supplyPrice = rec[prefix + '_supply_price']
|
||||
return {
|
||||
sales: sales != null ? String(sales) : '-',
|
||||
salesPrice: formatMoney(salesPrice),
|
||||
supplyPrice: formatMoney(supplyPrice),
|
||||
}
|
||||
}
|
||||
|
||||
export function mapReconciliationStoreRow(item) {
|
||||
if (!item) return {}
|
||||
const rec = item.reconciliation || {}
|
||||
return {
|
||||
id: item.id,
|
||||
name: item.name || '-',
|
||||
totalSales: formatMoney(rec.total_sales_price),
|
||||
totalSupply: formatMoney(rec.total_supply_price),
|
||||
herbalSalesPrice: formatMoney(rec.herbal_sales_price),
|
||||
medicineSalesPrice: formatMoney(rec.medicine_sales_price),
|
||||
registrationPrice: formatMoney(rec.registration_price),
|
||||
herbal: mapReconciliationCategory(rec, 'herbal'),
|
||||
medicine: mapReconciliationCategory(rec, 'medicine'),
|
||||
servicePackage: mapReconciliationCategory(rec, 'service_package'),
|
||||
otherFees: {
|
||||
express: formatMoney(rec.express_price),
|
||||
process: formatMoney(rec.process_price),
|
||||
treatment: formatMoney(rec.treatment_price),
|
||||
},
|
||||
_raw: item,
|
||||
}
|
||||
}
|
||||
|
||||
export function mapReconciliationDrugRow(item) {
|
||||
if (!item) return {}
|
||||
return {
|
||||
drugId: item.drug_id,
|
||||
drugName: item.drug_name || '-',
|
||||
drugNumber: item.drug_number || '',
|
||||
typeText: item.type_txt || '-',
|
||||
number: item.number != null ? item.number : '-',
|
||||
marketPrice: formatMoney(item.market_price),
|
||||
totalSales: formatMoney(item.total_sales_price),
|
||||
totalSupply: formatMoney(item.total_supply_price),
|
||||
orderNo: item.order_no || '',
|
||||
_raw: item,
|
||||
}
|
||||
}
|
||||
|
||||
export function formatPricePlain(v) {
|
||||
if (v === null || v === undefined || v === '') return '-'
|
||||
return '¥' + Number(v).toFixed(2)
|
||||
}
|
||||
|
||||
/** 汇总项:隐藏全零噪音 */
|
||||
export function filterReconciliationStatItems(items) {
|
||||
return items.filter(s => {
|
||||
if (s.key === 'total_sales_price' || s.key === 'total_supply_price') return true
|
||||
if (s.isCount) {
|
||||
const n = Number(s.raw)
|
||||
return !Number.isNaN(n) && n !== 0
|
||||
}
|
||||
const v = s.raw
|
||||
if (v === null || v === undefined || v === '') return false
|
||||
const n = Number(v)
|
||||
return !Number.isNaN(n) && n !== 0
|
||||
})
|
||||
}
|
||||
|
||||
/** 诊所对账汇总(对齐 PC setStoreStatistics) */
|
||||
export function mapReconciliationStoreCount(count) {
|
||||
if (!count) return []
|
||||
const items = [
|
||||
{ key: 'total_sales_price', label: '总销售金额', value: formatPricePlain(count.total_sales_price), raw: count.total_sales_price },
|
||||
{ key: 'total_supply_price', label: '总供货金额', value: formatPricePlain(count.total_supply_price), raw: count.total_supply_price },
|
||||
{ key: 'herbal_sales', label: '草药销量', value: count.herbal_sales != null ? String(count.herbal_sales) : '-', raw: count.herbal_sales, isCount: true },
|
||||
{ key: 'herbal_sales_price', label: '草药销售金额', value: formatPricePlain(count.herbal_sales_price), raw: count.herbal_sales_price },
|
||||
{ key: 'herbal_supply_price', label: '草药供货金额', value: formatPricePlain(count.herbal_supply_price), raw: count.herbal_supply_price },
|
||||
{ key: 'medicine_sales', label: '成药销量', value: count.medicine_sales != null ? String(count.medicine_sales) : '-', raw: count.medicine_sales, isCount: true },
|
||||
{ key: 'medicine_sales_price', label: '成药销售金额', value: formatPricePlain(count.medicine_sales_price), raw: count.medicine_sales_price },
|
||||
{ key: 'medicine_supply_price', label: '成药供货金额', value: formatPricePlain(count.medicine_supply_price), raw: count.medicine_supply_price },
|
||||
{ key: 'express_price', label: '快递费', value: formatPricePlain(count.express_price), raw: count.express_price },
|
||||
{ key: 'process_price', label: '加工费', value: formatPricePlain(count.process_price), raw: count.process_price },
|
||||
{ key: 'treatment_price', label: '诊疗费', value: formatPricePlain(count.treatment_price), raw: count.treatment_price },
|
||||
{ key: 'outpatient_price', label: '门诊收入', value: formatPricePlain(count.outpatient_price), raw: count.outpatient_price },
|
||||
{ key: 'registration_price', label: '挂号费', value: formatPricePlain(count.registration_price), raw: count.registration_price },
|
||||
]
|
||||
return filterReconciliationStatItems(items)
|
||||
}
|
||||
|
||||
/** 药品对账汇总(对齐 PC setDrugStatistics) */
|
||||
export function mapReconciliationDrugCount(count) {
|
||||
if (!count) return []
|
||||
return [
|
||||
{ key: 'total_sales_price', label: '总销售金额', value: formatPricePlain(count.total_sales_price) },
|
||||
{ key: 'total_supply_price', label: '总供货金额', value: formatPricePlain(count.total_supply_price) },
|
||||
{ key: 'number', label: '销量', value: count.number != null ? String(count.number) : '-' },
|
||||
]
|
||||
}
|
||||
121
subPackages/sub_clinic_admin/common/filterCache.js
Normal file
121
subPackages/sub_clinic_admin/common/filterCache.js
Normal file
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* 诊所端列表筛选条件本地缓存(对齐 PC reconciliation formCache)
|
||||
*/
|
||||
|
||||
import {
|
||||
defaultMonthStartDate,
|
||||
defaultTodayDate,
|
||||
} from './reconciliationParams.js'
|
||||
|
||||
const KEY_ORDER = 'clinic_admin.order.filter'
|
||||
const KEY_RECONCILIATION = 'clinic_admin.reconciliation.filter'
|
||||
|
||||
function readJson(key) {
|
||||
try {
|
||||
const raw = uni.getStorageSync(key)
|
||||
if (!raw) return null
|
||||
return typeof raw === 'string' ? JSON.parse(raw) : raw
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function writeJson(key, data) {
|
||||
uni.setStorageSync(key, JSON.stringify(data))
|
||||
}
|
||||
|
||||
export function defaultOrderFilter() {
|
||||
return {
|
||||
orderNo: '',
|
||||
statusIndex: 0,
|
||||
deliveryIndex: 0,
|
||||
prescriptionTypeIndex: 0,
|
||||
dateStart: defaultMonthStartDate(),
|
||||
dateEnd: defaultTodayDate(),
|
||||
}
|
||||
}
|
||||
|
||||
export function loadOrderFilter() {
|
||||
const cached = readJson(KEY_ORDER)
|
||||
if (!cached || typeof cached !== 'object') return defaultOrderFilter()
|
||||
const d = defaultOrderFilter()
|
||||
return {
|
||||
orderNo: cached.orderNo != null ? String(cached.orderNo) : d.orderNo,
|
||||
statusIndex: Number(cached.statusIndex) || 0,
|
||||
deliveryIndex: Number(cached.deliveryIndex) || 0,
|
||||
prescriptionTypeIndex: Number(cached.prescriptionTypeIndex) || 0,
|
||||
dateStart: cached.dateStart || d.dateStart,
|
||||
dateEnd: cached.dateEnd || d.dateEnd,
|
||||
}
|
||||
}
|
||||
|
||||
export function saveOrderFilter(data) {
|
||||
writeJson(KEY_ORDER, {
|
||||
orderNo: data.orderNo || '',
|
||||
statusIndex: data.statusIndex,
|
||||
deliveryIndex: data.deliveryIndex,
|
||||
prescriptionTypeIndex: data.prescriptionTypeIndex,
|
||||
dateStart: data.dateStart,
|
||||
dateEnd: data.dateEnd,
|
||||
})
|
||||
}
|
||||
|
||||
export function clearOrderFilter() {
|
||||
try {
|
||||
uni.removeStorageSync(KEY_ORDER)
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
export function defaultReconciliationFilter() {
|
||||
return {
|
||||
tabIndex: 0,
|
||||
dateStart: defaultMonthStartDate(),
|
||||
dateEnd: defaultTodayDate(),
|
||||
byOrder: false,
|
||||
drugSearchQ: '',
|
||||
orderSearchQ: '',
|
||||
drugId: null,
|
||||
orderId: null,
|
||||
selectedDrugLabel: '',
|
||||
selectedOrderLabel: '',
|
||||
}
|
||||
}
|
||||
|
||||
export function loadReconciliationFilter() {
|
||||
const cached = readJson(KEY_RECONCILIATION)
|
||||
if (!cached || typeof cached !== 'object') return defaultReconciliationFilter()
|
||||
const d = defaultReconciliationFilter()
|
||||
return {
|
||||
tabIndex: cached.tabIndex === 1 ? 1 : 0,
|
||||
dateStart: cached.dateStart || d.dateStart,
|
||||
dateEnd: cached.dateEnd || d.dateEnd,
|
||||
byOrder: !!cached.byOrder,
|
||||
drugSearchQ: cached.drugSearchQ != null ? String(cached.drugSearchQ) : '',
|
||||
orderSearchQ: cached.orderSearchQ != null ? String(cached.orderSearchQ) : '',
|
||||
drugId: cached.drugId != null && cached.drugId !== '' ? Number(cached.drugId) : null,
|
||||
orderId: cached.orderId != null && cached.orderId !== '' ? Number(cached.orderId) : null,
|
||||
selectedDrugLabel: cached.selectedDrugLabel || '',
|
||||
selectedOrderLabel: cached.selectedOrderLabel || '',
|
||||
}
|
||||
}
|
||||
|
||||
export function saveReconciliationFilter(data) {
|
||||
writeJson(KEY_RECONCILIATION, {
|
||||
tabIndex: data.tabIndex,
|
||||
dateStart: data.dateStart,
|
||||
dateEnd: data.dateEnd,
|
||||
byOrder: data.byOrder,
|
||||
drugSearchQ: data.drugSearchQ || '',
|
||||
orderSearchQ: data.orderSearchQ || '',
|
||||
drugId: data.drugId,
|
||||
orderId: data.orderId,
|
||||
selectedDrugLabel: data.selectedDrugLabel || '',
|
||||
selectedOrderLabel: data.selectedOrderLabel || '',
|
||||
})
|
||||
}
|
||||
|
||||
export function clearReconciliationFilter() {
|
||||
try {
|
||||
uni.removeStorageSync(KEY_RECONCILIATION)
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
7
subPackages/sub_clinic_admin/common/format.js
Normal file
7
subPackages/sub_clinic_admin/common/format.js
Normal file
@@ -0,0 +1,7 @@
|
||||
/** 金额展示(WXML 模板用 methods 调用) */
|
||||
export function formatMoney(v) {
|
||||
if (v === null || v === undefined || v === '' || v === '-') return '-'
|
||||
const n = Number(v)
|
||||
if (Number.isNaN(n)) return String(v)
|
||||
return '¥' + n.toFixed(2)
|
||||
}
|
||||
32
subPackages/sub_clinic_admin/common/orderParams.js
Normal file
32
subPackages/sub_clinic_admin/common/orderParams.js
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 商品订单请求参数(对齐 PC product-order/config/search.ts)
|
||||
*/
|
||||
|
||||
import { buildSearchTimeQueryParams } from './reconciliationParams.js'
|
||||
|
||||
export function buildOrderListParams({
|
||||
page = 1,
|
||||
pageSize = 15,
|
||||
orderNo = '',
|
||||
status,
|
||||
deliveryMethod,
|
||||
prescriptionType,
|
||||
dateStart,
|
||||
dateEnd,
|
||||
}) {
|
||||
const params = {
|
||||
page,
|
||||
pageSize,
|
||||
...buildSearchTimeQueryParams(dateStart, dateEnd),
|
||||
}
|
||||
if (orderNo) params.order_no = orderNo
|
||||
if (status !== '' && status != null) params.status = status
|
||||
if (deliveryMethod !== '' && deliveryMethod != null) params.delivery_method = deliveryMethod
|
||||
if (prescriptionType !== '' && prescriptionType != null) params.prescription_type = prescriptionType
|
||||
return params
|
||||
}
|
||||
|
||||
export function buildOrderSaleAmountParams(opts) {
|
||||
const { page, pageSize, ...rest } = opts || {}
|
||||
return buildOrderListParams(rest)
|
||||
}
|
||||
70
subPackages/sub_clinic_admin/common/reconciliationParams.js
Normal file
70
subPackages/sub_clinic_admin/common/reconciliationParams.js
Normal file
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* 对账请求参数(对齐 PC finance/reconciliation/index.vue)
|
||||
*/
|
||||
|
||||
export const STORE_LIST_PAGE_SIZE = 9999
|
||||
|
||||
/** Laravel GET 数组:search_time[0] / search_time[1](uni.request 勿传 JS 数组,否则会变成逗号字符串) */
|
||||
export function buildSearchTimeQueryParams(dateStart, dateEnd) {
|
||||
return {
|
||||
'search_time[0]': dateStart,
|
||||
'search_time[1]': dateEnd,
|
||||
}
|
||||
}
|
||||
|
||||
export function buildStoreListParams({
|
||||
page = 1,
|
||||
pageSize = STORE_LIST_PAGE_SIZE,
|
||||
dateStart,
|
||||
dateEnd,
|
||||
storeName = '',
|
||||
excludeZeroSales = 0,
|
||||
}) {
|
||||
const params = {
|
||||
page,
|
||||
pageSize,
|
||||
exclude_zero_sales: excludeZeroSales,
|
||||
...buildSearchTimeQueryParams(dateStart, dateEnd),
|
||||
}
|
||||
if (storeName) params.store_name = storeName
|
||||
return params
|
||||
}
|
||||
|
||||
export function buildDrugListParams({
|
||||
dateStart,
|
||||
dateEnd,
|
||||
drugId,
|
||||
orderId,
|
||||
byOrder = false,
|
||||
storeId,
|
||||
}) {
|
||||
const params = {
|
||||
...buildSearchTimeQueryParams(dateStart, dateEnd),
|
||||
}
|
||||
if (drugId) params.drug_id = drugId
|
||||
if (orderId) params.order_id = orderId
|
||||
if (byOrder) params.by_order = 1
|
||||
if (storeId != null) params.store_id = storeId
|
||||
return params
|
||||
}
|
||||
|
||||
export function normalizeReconciliationResponse(data) {
|
||||
const d = data || {}
|
||||
const listWrap = d.list || {}
|
||||
const raw = listWrap.items
|
||||
return {
|
||||
count: d.count || {},
|
||||
items: Array.isArray(raw) ? raw : [],
|
||||
pageMeta: listWrap,
|
||||
}
|
||||
}
|
||||
|
||||
/** 本月 1 日 YYYY-MM-DD */
|
||||
export function defaultMonthStartDate() {
|
||||
const d = new Date()
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-01`
|
||||
}
|
||||
|
||||
export function defaultTodayDate() {
|
||||
return new Date().toISOString().slice(0, 10)
|
||||
}
|
||||
28
subPackages/sub_clinic_admin/common/tabbar.js
Normal file
28
subPackages/sub_clinic_admin/common/tabbar.js
Normal file
@@ -0,0 +1,28 @@
|
||||
export const TAB_WORKBENCH = 0
|
||||
export const TAB_MY = 1
|
||||
|
||||
export const CLINIC_TAB_ROUTES = [
|
||||
'/subPackages/sub_clinic_admin/home/index',
|
||||
'/subPackages/sub_clinic_admin/my/index',
|
||||
]
|
||||
|
||||
export function getClinicTabList() {
|
||||
return [
|
||||
{
|
||||
iconPath: require('@/static/image/w-sf.png'),
|
||||
selectedIconPath: require('@/static/image/sf.png'),
|
||||
text: '工作台',
|
||||
},
|
||||
{
|
||||
iconPath: require('@/static/image/w-wd.png'),
|
||||
selectedIconPath: require('@/static/image/wd.png'),
|
||||
text: '我的',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
export function switchClinicTab(index) {
|
||||
const url = CLINIC_TAB_ROUTES[index]
|
||||
if (!url) return
|
||||
uni.reLaunch({ url })
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<view class="filter-panel filter-panel-sticky">
|
||||
<view class="filter-head">
|
||||
<view class="filter-head-left" @click="toggle">
|
||||
<text class="filter-title">{{ title }}</text>
|
||||
<text class="filter-toggle">{{ expanded ? '收起' : '展开' }}</text>
|
||||
</view>
|
||||
<text v-if="showClear" class="filter-clear-btn" @click.stop="onClear">清空</text>
|
||||
</view>
|
||||
<view v-show="expanded" class="filter-body">
|
||||
<slot />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'CollapsibleFilterPanel',
|
||||
props: {
|
||||
title: { type: String, default: '筛选条件' },
|
||||
defaultExpanded: { type: Boolean, default: false },
|
||||
showClear: { type: Boolean, default: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
expanded: this.defaultExpanded,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggle() {
|
||||
this.expanded = !this.expanded
|
||||
},
|
||||
onClear() {
|
||||
this.$emit('clear')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.filter-panel {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 24rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
.filter-panel-sticky {
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
.filter-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx;
|
||||
}
|
||||
.filter-head-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
gap: 16rpx;
|
||||
}
|
||||
.filter-title {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
.filter-toggle {
|
||||
font-size: 26rpx;
|
||||
color: #6acdbb;
|
||||
}
|
||||
.filter-clear-btn {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
padding: 8rpx 0 8rpx 24rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.filter-body {
|
||||
padding: 0 24rpx 24rpx;
|
||||
}
|
||||
</style>
|
||||
21
subPackages/sub_clinic_admin/components/PageStickyHeader.vue
Normal file
21
subPackages/sub_clinic_admin/components/PageStickyHeader.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<view class="page-sticky-header">
|
||||
<slot />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'PageStickyHeader',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-sticky-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
background: #f9fafb;
|
||||
padding-bottom: 8rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<view class="clinic-layout safe-area-inset-bottom">
|
||||
<u-navbar
|
||||
:title="title"
|
||||
:is-back="showBack"
|
||||
title-size="34"
|
||||
title-color="#fff"
|
||||
:background="{ background: 'linear-gradient(90deg, #00BFA6 0%, #6ACDBB 93%)' }"
|
||||
/>
|
||||
<view class="clinic-body" :style="bodyStyle">
|
||||
<slot />
|
||||
</view>
|
||||
<u-tabbar
|
||||
v-if="showTabbar"
|
||||
:value="tabIndex"
|
||||
:list="tabList"
|
||||
active-color="#6ACDBB"
|
||||
@change="onTabChange"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getClinicTabList, switchClinicTab } from '../common/tabbar.js'
|
||||
|
||||
export default {
|
||||
name: 'ClinicPageLayout',
|
||||
props: {
|
||||
title: { type: String, default: '' },
|
||||
showBack: { type: Boolean, default: false },
|
||||
showTabbar: { type: Boolean, default: false },
|
||||
tabIndex: { type: Number, default: 0 },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
statusBarHeight: 0,
|
||||
navbarHeight: 0,
|
||||
tabList: [],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
bodyStyle() {
|
||||
const top = this.statusBarHeight + this.navbarHeight
|
||||
let bottom = '32rpx'
|
||||
if (this.showTabbar) {
|
||||
bottom = 'calc(100rpx + env(safe-area-inset-bottom))'
|
||||
}
|
||||
return {
|
||||
paddingTop: top + 'px',
|
||||
paddingBottom: bottom,
|
||||
minHeight: '100vh',
|
||||
boxSizing: 'border-box',
|
||||
}
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.statusBarHeight = this.$statusBarHeight || 0
|
||||
this.navbarHeight = this.$navbarHeight || 44
|
||||
if (this.showTabbar) {
|
||||
this.tabList = getClinicTabList()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onTabChange(index) {
|
||||
if (index === this.tabIndex) return
|
||||
switchClinicTab(index)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.clinic-layout {
|
||||
min-height: 100vh;
|
||||
background: #f9fafb;
|
||||
}
|
||||
.clinic-body {
|
||||
padding-left: 24rpx;
|
||||
padding-right: 24rpx;
|
||||
}
|
||||
</style>
|
||||
1
subPackages/sub_clinic_admin/home/index.vue
Normal file
1
subPackages/sub_clinic_admin/home/index.vue
Normal file
File diff suppressed because one or more lines are too long
212
subPackages/sub_clinic_admin/my/index.vue
Normal file
212
subPackages/sub_clinic_admin/my/index.vue
Normal file
@@ -0,0 +1,212 @@
|
||||
<template>
|
||||
<clinic-page-layout title="我的" :show-tabbar="true" :tab-index="1">
|
||||
<view class="mine-container">
|
||||
|
||||
<!-- 个人信息面板(通栏) -->
|
||||
<view class="profile-panel">
|
||||
<!-- 超椭圆高质感头像 -->
|
||||
<view class="avatar-squircle">
|
||||
<text>{{ avatarText }}</text>
|
||||
</view>
|
||||
|
||||
<view class="user-info">
|
||||
<text class="user-name">{{ displayName }}</text>
|
||||
<!-- 角色标签:替代普通文本,提升专业感 -->
|
||||
<view class="role-badge">
|
||||
<text class="role-text">诊所管理员</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 信息与设置面板(通栏) -->
|
||||
<view class="setting-panel">
|
||||
<view class="setting-item">
|
||||
<text class="item-label">所属诊所</text>
|
||||
<text class="item-value">{{ storeName }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作面板(通栏) -->
|
||||
<view class="setting-panel">
|
||||
<view
|
||||
class="setting-item action-item"
|
||||
hover-class="item-hover"
|
||||
:hover-stay-time="100"
|
||||
@click="logout"
|
||||
>
|
||||
<text class="danger-text">退出登录</text>
|
||||
<!-- 保留原有的 u-icon -->
|
||||
<u-icon name="arrow-right" color="#C9CDD4" size="28" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</clinic-page-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ClinicPageLayout from '../components/clinic-page-layout.vue'
|
||||
|
||||
export default {
|
||||
components: { ClinicPageLayout },
|
||||
data() {
|
||||
return {
|
||||
displayName: '',
|
||||
storeName: '',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
avatarText() {
|
||||
const n = this.displayName || '管'
|
||||
return n.slice(0, 1)
|
||||
},
|
||||
},
|
||||
onShow() {
|
||||
const user = uni.getStorageSync('clinic_admin_user') || {}
|
||||
this.displayName = user.nick_name || user.name || user.account || '管理员'
|
||||
this.storeName = user.store_name || (user.store && user.store.name) || '-'
|
||||
},
|
||||
methods: {
|
||||
logout() {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定退出登录?',
|
||||
confirmColor: '#f53f3f', // 将确认按钮改为警示红,提升用户体验
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
uni.removeStorageSync('clinic_admin_token')
|
||||
uni.removeStorageSync('clinic_admin_user')
|
||||
uni.removeStorageSync('loginMode')
|
||||
uni.reLaunch({ url: '/pages/login/index' })
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 保持与工作台完全一致的色彩变量 */
|
||||
$theme-primary: #6acdbb;
|
||||
$theme-dark: #4db6a8;
|
||||
$color-page-bg: #F5F7F8;
|
||||
$color-text-title: #222B2A;
|
||||
$color-text-body: #4E5969;
|
||||
$color-text-muted: #86909C;
|
||||
$color-border: #F2F3F5;
|
||||
$color-danger: #F53F3F; /* 现代系统标准红 */
|
||||
|
||||
.mine-container {
|
||||
padding: 0 0 60rpx;
|
||||
background-color: $color-page-bg;
|
||||
min-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 个人信息面板:通栏纯白 */
|
||||
.profile-panel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #ffffff;
|
||||
padding: 60rpx 40rpx 50rpx; /* 顶部增加呼吸感 */
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
/* 超椭圆头像:抛弃死板的纯圆,注入品牌色和微弱发光质感 */
|
||||
.avatar-squircle {
|
||||
width: 128rpx;
|
||||
height: 128rpx;
|
||||
border-radius: 40rpx; /* 超椭圆倒角 */
|
||||
background: linear-gradient(135deg, $theme-primary 0%, $theme-dark 100%);
|
||||
box-shadow: 0 8rpx 24rpx rgba(106, 205, 187, 0.25);
|
||||
color: #ffffff;
|
||||
font-size: 52rpx;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
margin-left: 32rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
color: $color-text-title;
|
||||
margin-bottom: 12rpx;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
|
||||
/* 角色标签:用线框+背景色取代纯文本,更具系统身份感 */
|
||||
.role-badge {
|
||||
align-self: flex-start;
|
||||
background-color: rgba(106, 205, 187, 0.1);
|
||||
border: 1rpx solid rgba(106, 205, 187, 0.2);
|
||||
padding: 4rpx 16rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.role-text {
|
||||
font-size: 22rpx;
|
||||
color: $theme-dark;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 设置项面板(通栏组) */
|
||||
.setting-panel {
|
||||
background-color: #ffffff;
|
||||
margin-bottom: 24rpx;
|
||||
padding: 0 40rpx;
|
||||
}
|
||||
|
||||
/* 单个设置项 */
|
||||
.setting-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 36rpx 0;
|
||||
border-bottom: 1rpx solid $color-border;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
/* 移除组内最后一个元素的底边框 */
|
||||
.setting-panel .setting-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.item-label {
|
||||
font-size: 30rpx;
|
||||
color: $color-text-title;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.item-value {
|
||||
font-size: 30rpx;
|
||||
color: $color-text-muted;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* 交互列表项 */
|
||||
.action-item {
|
||||
/* 扩大点击热区补偿 padding */
|
||||
margin: 0 -40rpx;
|
||||
padding: 36rpx 40rpx;
|
||||
}
|
||||
|
||||
.item-hover {
|
||||
background-color: #F7F8FA;
|
||||
}
|
||||
|
||||
/* 退出登录专用红色 */
|
||||
.danger-text {
|
||||
font-size: 30rpx;
|
||||
color: $color-danger;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<view v-if="showBlock" class="express-panel">
|
||||
<view class="section-title">物流信息</view>
|
||||
<template v-if="display.hasData">
|
||||
<view class="row"><text>物流公司</text><text>{{ display.companyName }}</text></view>
|
||||
<view class="row"><text>运单号</text><text>{{ display.expressNo }}</text></view>
|
||||
<view class="row"><text>最新状态</text><text class="highlight-text">{{ display.stateText }}</text></view>
|
||||
|
||||
<view v-if="display.tracks.length" class="timeline">
|
||||
<view class="timeline-title">物流追踪</view>
|
||||
<view v-for="(t, idx) in display.tracks" :key="idx" class="track-item">
|
||||
<view class="track-dot" :class="{ 'active': idx === 0 }" />
|
||||
<view class="track-body">
|
||||
<text class="track-status" :class="{ 'active': idx === 0 }">{{ t.status }}</text>
|
||||
<text class="track-detail">{{ t.detail }}</text>
|
||||
<text class="track-time">{{ t.time }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<view v-else class="empty-express">暂无物流信息</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapExpressDetail } from '../../common/display.js'
|
||||
|
||||
export default {
|
||||
name: 'ExpressTimeline',
|
||||
props: {
|
||||
express: { type: Object, default: null },
|
||||
show: { type: Boolean, default: true },
|
||||
},
|
||||
computed: {
|
||||
showBlock() {
|
||||
return this.show
|
||||
},
|
||||
display() {
|
||||
return mapExpressDetail(this.express)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$theme-primary: #6acdbb;
|
||||
$color-text-title: #222B2A;
|
||||
$color-text-body: #4E5969;
|
||||
$color-text-muted: #86909C;
|
||||
$color-border: #F2F3F5;
|
||||
|
||||
.express-panel {
|
||||
background: #ffffff;
|
||||
padding: 32rpx 40rpx;
|
||||
margin-bottom: 24rpx;
|
||||
/* 移除圆角和阴影,采用通栏设计 */
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: $color-text-title;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx 0;
|
||||
font-size: 28rpx;
|
||||
border-bottom: 1rpx solid $color-border;
|
||||
gap: 24rpx;
|
||||
|
||||
text:first-child {
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
text:last-child {
|
||||
color: $color-text-title;
|
||||
text-align: right;
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.highlight-text {
|
||||
color: $theme-primary;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.timeline-title {
|
||||
font-size: 28rpx;
|
||||
color: $color-text-title;
|
||||
font-weight: 600;
|
||||
margin: 32rpx 0 24rpx;
|
||||
}
|
||||
|
||||
.track-item {
|
||||
display: flex;
|
||||
padding: 0;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.track-dot {
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 50%;
|
||||
background: $color-border;
|
||||
margin-top: 12rpx;
|
||||
flex-shrink: 0;
|
||||
border: 4rpx solid #ffffff;
|
||||
box-shadow: 0 0 0 2rpx $color-border;
|
||||
|
||||
&.active {
|
||||
background: $theme-primary;
|
||||
box-shadow: 0 0 0 4rpx rgba(106, 205, 187, 0.2);
|
||||
border-color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
.track-body {
|
||||
flex: 1;
|
||||
margin-left: 28rpx;
|
||||
padding-bottom: 32rpx;
|
||||
border-left: 2rpx solid $color-border;
|
||||
padding-left: 28rpx;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
/* 最后一个元素去掉线 */
|
||||
.track-item:last-child .track-body {
|
||||
border-left-color: transparent;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.track-status {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: $color-text-body;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8rpx;
|
||||
|
||||
&.active {
|
||||
color: $theme-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.track-detail {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: $color-text-body;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.track-time {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
.empty-express {
|
||||
text-align: center;
|
||||
color: $color-text-muted;
|
||||
font-size: 26rpx;
|
||||
padding: 40rpx 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,591 @@
|
||||
<template>
|
||||
<u-popup v-model="show" mode="bottom" border-radius="20" height="85%" :closeable="true" @close="onClose">
|
||||
<view class="popup-wrap">
|
||||
<scroll-view v-if="view" scroll-y class="popup-scroll">
|
||||
<view class="container safe-area-inset-bottom">
|
||||
<!-- 处方头部 -->
|
||||
<view class="head">
|
||||
<view class="head_record_top">
|
||||
<view class="record">
|
||||
处方编号:{{ view.prescriptionNo }}
|
||||
</view>
|
||||
<view class="record_state">
|
||||
{{ view.typeText }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 动态医院与公章 -->
|
||||
<view class="head_record">
|
||||
<image :src="view.sealImage || '/static/group/xkyard.png'" mode="aspectFit"></image>
|
||||
<view class="record_yard">
|
||||
<view class="yard">
|
||||
{{ view.storeName }}
|
||||
</view>
|
||||
<view class="state">
|
||||
处方笺
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 时间 -->
|
||||
<view class="head_record_time">
|
||||
<text style="text-align: right;">开具日期:{{ view.createdAt }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 患者信息 -->
|
||||
<view class="my_info">
|
||||
<view class="info">
|
||||
<view class="info_item">
|
||||
<view class="name">姓名:<text>{{ view.patientName }}</text></view>
|
||||
</view>
|
||||
<view class="info_item">
|
||||
<view class="name">性别:<text>{{ view.patientSex }}</text></view>
|
||||
</view>
|
||||
<view class="info_item">
|
||||
<view class="name">年龄:<text>{{ view.patientAge }}</text></view>
|
||||
</view>
|
||||
<view class="info_item">
|
||||
<view class="name">类别:<text>{{ view.category }}</text></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="info">
|
||||
<view class="info_item">
|
||||
<view class="name">科室:<text>{{ view.departName }}</text></view>
|
||||
</view>
|
||||
<view class="info_item" v-if="view.patientMobile">
|
||||
<view class="name">电话:<text>{{ view.patientMobile }}</text></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="info">
|
||||
<view class="info_item">
|
||||
<view class="names">诊断:<text>{{ view.diagnose }}</text></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 中医专属字段 -->
|
||||
<view v-if="view.tcmSyndrome || view.tcmMethod || view.tcmDisease" class="info">
|
||||
<view v-if="view.tcmSyndrome" class="info_item">
|
||||
<view class="names">中医证候:<text>{{ view.tcmSyndrome }}</text></view>
|
||||
</view>
|
||||
<view v-if="view.tcmMethod" class="info_item">
|
||||
<view class="names">中医治法:<text>{{ view.tcmMethod }}</text></view>
|
||||
</view>
|
||||
<view v-if="view.tcmDisease" class="info_item">
|
||||
<view class="names">中医疾病:<text>{{ view.tcmDisease }}</text></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bg">
|
||||
<!-- 处方明细区 (Rp) -->
|
||||
<view class="my_medical">
|
||||
<view class="title">Rp</view>
|
||||
|
||||
<view v-for="(recipe, rIdx) in view.recipes" :key="rIdx" class="item">
|
||||
<view class="name">
|
||||
<view class="_name" v-for="(drug, dIdx) in recipe.drugs" :key="dIdx">
|
||||
<view class="text">
|
||||
<text>{{ drug.name }}</text>
|
||||
<text class="_abbr" v-if="drug.usage">[{{ drug.usage }}]</text>
|
||||
</view>
|
||||
<text class="name_num">{{ drug.qty }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="details" v-if="recipe.usage || recipe.process">
|
||||
<view class="text">
|
||||
<text v-if="recipe.usage">用法:{{ recipe.usage }}</text>
|
||||
<text v-if="recipe.process" style="margin-left: 10rpx;">包装方式:{{ recipe.process }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 医嘱 -->
|
||||
<view class="doctor_order">
|
||||
<view class="yz">
|
||||
<text class="order_info_left">医嘱:</text>
|
||||
<view class="order_info">
|
||||
<view class="info">{{ view.doctorOrder !== '-' ? view.doctorOrder : '无' }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="titles">处方开具已完毕</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 动态医生与药师签名区 -->
|
||||
<view class="my_doctor">
|
||||
<view class="doctor_info">
|
||||
<view class="info">
|
||||
<text>医师</text>
|
||||
<image v-if="view.doctorSignImage" :src="checkImageUrl(view.doctorSignImage)" mode="aspectFit"></image>
|
||||
<text v-else class="name">{{ view.doctorName }}</text>
|
||||
</view>
|
||||
<view class="info">
|
||||
<text>审核药师</text>
|
||||
<image v-if="view.pharmacistSignImage" :src="checkImageUrl(view.pharmacistSignImage)" mode="aspectFit"></image>
|
||||
<text v-else class="name"></text>
|
||||
</view>
|
||||
<view class="info">
|
||||
<text>发药人</text>
|
||||
<image v-if="view.pharmacistSignImage" :src="checkImageUrl(view.pharmacistSignImage)" mode="aspectFit"></image>
|
||||
<text v-else class="name"></text>
|
||||
</view>
|
||||
<view class="info">
|
||||
<text>核对人</text>
|
||||
<image v-if="view.pharmacistSignImage" :src="checkImageUrl(view.pharmacistSignImage)" mode="aspectFit"></image>
|
||||
<text v-else class="name"></text>
|
||||
</view>
|
||||
<view class="info">
|
||||
<text>调配人</text>
|
||||
<image v-if="view.pharmacistSignImage" :src="checkImageUrl(view.pharmacistSignImage)" mode="aspectFit"></image>
|
||||
<text v-else class="name"></text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 价格 -->
|
||||
<view class="price" v-if="view.totalPayPrice">
|
||||
价格 ¥{{ view.totalPayPrice }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 动态有效期温馨提示 -->
|
||||
<view class="my_prompt">
|
||||
<view class="title">
|
||||
温馨提示:请遵医嘱服药!处方{{ view.validHours }}小时有效!
|
||||
</view>
|
||||
<image src="/static/group/img-yzf.png" mode="aspectFit" v-if="view.status == 2"></image>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view v-else-if="loading" class="popup-loading">加载中...</view>
|
||||
<view v-else class="popup-loading">暂无数据</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getPrescriptionDetail } from '@/api/clinicAdmin.js'
|
||||
import { mapPrescriptionDetailView } from '../../common/display.js'
|
||||
|
||||
export default {
|
||||
name: 'PrescriptionDetailPopup',
|
||||
data() {
|
||||
return {
|
||||
show: false,
|
||||
loading: false,
|
||||
view: null,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 解析签名图片,自动兼容 base64 和 http 链接
|
||||
checkImageUrl(url) {
|
||||
if (!url) return ''
|
||||
return url.includes('http') ? url : 'data:image/jpeg;base64,' + url
|
||||
},
|
||||
open(prescriptionId) {
|
||||
if (!prescriptionId) return
|
||||
this.show = true
|
||||
this.view = null
|
||||
this.loading = true
|
||||
getPrescriptionDetail(prescriptionId).then(w => {
|
||||
if (w.ok && w.data) {
|
||||
// 复用原有的处方解析逻辑
|
||||
const mapped = mapPrescriptionDetailView(w.data)
|
||||
|
||||
// --- 动态注入你新版 UI 需要的独有字段 ---
|
||||
mapped.storeName = w.data.store ? w.data.store.name : '未知诊所'
|
||||
mapped.sealImage = w.data.store ? w.data.store.offical_seal : ''
|
||||
mapped.typeText = w.data.type == 1 ? '普通' : '常用'
|
||||
mapped.totalPayPrice = w.data.total_pay_price || '0.00'
|
||||
mapped.doctorSignImage = w.data.doctor_sign_image || ''
|
||||
mapped.pharmacistSignImage = w.data.Pharmacist_sign_image || ''
|
||||
mapped.validHours = w.data.valid_hours || 72
|
||||
mapped.status = w.data.status
|
||||
mapped.patientMobile = w.data.patient ? w.data.patient.mobile : ''
|
||||
|
||||
this.view = mapped
|
||||
}
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
onClose() {
|
||||
this.view = null
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.popup-wrap {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #fff;
|
||||
position: relative;
|
||||
padding-top: 60rpx; /* 给默认的关闭按钮留出空间 */
|
||||
}
|
||||
|
||||
.popup-scroll {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.popup-loading {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding: 80rpx;
|
||||
}
|
||||
|
||||
/* --- 原汁原味的 prescriptionDetail.vue 核心样式 (自适应修复版) --- */
|
||||
.container {
|
||||
width: 100%;
|
||||
padding: 0 20rpx 40rpx;
|
||||
box-sizing: border-box;
|
||||
|
||||
.bg {
|
||||
min-height: 400rpx;
|
||||
}
|
||||
|
||||
.head {
|
||||
.head_record_top {
|
||||
width: 100%;
|
||||
margin: 14rpx auto 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.record {
|
||||
font-size: 28rpx;
|
||||
font-family: PingFang SC-Regular, PingFang SC;
|
||||
font-weight: 400;
|
||||
color: #6C7380;
|
||||
}
|
||||
|
||||
.record_state {
|
||||
width: 80rpx;
|
||||
height: 42rpx;
|
||||
line-height: 42rpx;
|
||||
text-align: center;
|
||||
font-size: 24rpx;
|
||||
font-family: PingFang SC-Regular, PingFang SC;
|
||||
font-weight: 400;
|
||||
color: #A7ABB0;
|
||||
border: 1rpx solid #C4C7CC;
|
||||
}
|
||||
}
|
||||
|
||||
.head_record {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
|
||||
image {
|
||||
width: 160rpx;
|
||||
height: 150rpx;
|
||||
position: absolute;
|
||||
left: 40%; /* 也可以根据需要调整居中印章 */
|
||||
top: -8rpx;
|
||||
opacity: 0.2; /* 添加微透明度防止完全遮盖文字 */
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.record_yard {
|
||||
text-align: center;
|
||||
z-index: 1;
|
||||
|
||||
.yard {
|
||||
font-size: 46rpx;
|
||||
font-family: PingFang SC-Regular, PingFang SC;
|
||||
font-weight: 400;
|
||||
color: #31353D;
|
||||
}
|
||||
|
||||
.state {
|
||||
height: 44rpx;
|
||||
font-size: 32rpx;
|
||||
font-family: PingFang SC-Regular, PingFang SC;
|
||||
font-weight: 400;
|
||||
color: #31353D;
|
||||
line-height: 38rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.head_record_time {
|
||||
text-align: right;
|
||||
margin-top: 20rpx;
|
||||
width: 100%;
|
||||
height: 34rpx;
|
||||
font-size: 24rpx;
|
||||
font-family: PingFang SC-Regular, PingFang SC;
|
||||
font-weight: 400;
|
||||
color: #6C7380;
|
||||
line-height: 28rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// 经典信息区
|
||||
.my_info {
|
||||
width: 100%;
|
||||
margin: 4rpx auto;
|
||||
border-top: 1rpx solid #31353D;
|
||||
border-bottom: 1rpx solid #31353D;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
padding: 4rpx 4rpx;
|
||||
|
||||
.info {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.info_item {
|
||||
.names {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
font-size: 28rpx;
|
||||
font-family: PingFang SC-Regular, PingFang SC;
|
||||
font-weight: 400;
|
||||
color: #6C7380;
|
||||
|
||||
text {
|
||||
flex: 1;
|
||||
display: block;
|
||||
height: 40rpx;
|
||||
font-size: 28rpx;
|
||||
font-family: PingFang SC-Regular, PingFang SC;
|
||||
font-weight: 400;
|
||||
color: #31353D;
|
||||
line-height: 40rpx;
|
||||
margin: 0 0rpx 0 8rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
font-size: 28rpx;
|
||||
font-family: PingFang SC-Regular, PingFang SC;
|
||||
font-weight: 400;
|
||||
color: #6C7380;
|
||||
|
||||
text {
|
||||
flex: 1;
|
||||
display: block;
|
||||
height: 40rpx;
|
||||
font-size: 28rpx;
|
||||
font-family: PingFang SC-Regular, PingFang SC;
|
||||
font-weight: 400;
|
||||
color: #31353D;
|
||||
line-height: 40rpx;
|
||||
margin: 0 26rpx 0 6rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 药品列表
|
||||
.my_medical {
|
||||
width: 100%;
|
||||
margin: 4rpx auto;
|
||||
padding: 1rpx 10rpx;
|
||||
box-sizing: border-box;
|
||||
|
||||
.title {
|
||||
font-size: 32rpx;
|
||||
font-family: PingFang SC-Semibold, PingFang SC;
|
||||
font-weight: 600;
|
||||
color: #31353D;
|
||||
margin-bottom: 2rpx;
|
||||
}
|
||||
|
||||
.item {
|
||||
width: 100%;
|
||||
padding: 1rpx 0;
|
||||
|
||||
.name {
|
||||
font-size: 28rpx;
|
||||
font-family: PingFang SC-Regular, PingFang SC;
|
||||
font-weight: 400;
|
||||
color: #31353D;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
._name {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 2rpx 80rpx 2rpx 0;
|
||||
|
||||
.text {
|
||||
margin-right: 16rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
._abbr {
|
||||
color: #A7ABB0;
|
||||
font-size: 20rpx;
|
||||
margin-top: 2rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.details {
|
||||
font-size: 28rpx;
|
||||
font-family: PingFang SC-Regular, PingFang SC;
|
||||
font-weight: 400;
|
||||
color: #6C7380;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
|
||||
.text {
|
||||
text {
|
||||
word-spacing: 1rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 医嘱
|
||||
.doctor_order {
|
||||
width: 100%;
|
||||
margin: 2rpx auto;
|
||||
padding: 0 10rpx;
|
||||
word-spacing: 2rpx;
|
||||
box-sizing: border-box;
|
||||
|
||||
.yz {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
color: #6C7380;
|
||||
|
||||
.order_info_left {
|
||||
margin-right: 3rpx;
|
||||
}
|
||||
|
||||
.order_info {
|
||||
font-size: 28rpx;
|
||||
font-family: PingFang SC-Regular, PingFang SC;
|
||||
font-weight: 400;
|
||||
line-height: 40rpx;
|
||||
margin-bottom: 2rpx;
|
||||
|
||||
.info {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.titles {
|
||||
width: 100%;
|
||||
font-size: 24rpx;
|
||||
font-family: PingFang SC-Regular, PingFang SC;
|
||||
font-weight: 400;
|
||||
color: #A7ABB0;
|
||||
margin: 8rpx 0 20rpx;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
// 电子签名及价格
|
||||
.my_doctor {
|
||||
width: 100%;
|
||||
margin: 2rpx auto;
|
||||
border-top: 1rpx solid #31353D;
|
||||
border-bottom: 1rpx solid #31353D;
|
||||
padding: 2rpx 4rpx;
|
||||
box-sizing: border-box;
|
||||
|
||||
.doctor_info {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 4rpx;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.info {
|
||||
min-width: 222rpx;
|
||||
max-width: 400rpx;
|
||||
margin: 5rpx 0 8rpx;
|
||||
|
||||
text {
|
||||
font-size: 24rpx;
|
||||
font-family: PingFang SC-Regular, PingFang SC;
|
||||
font-weight: 400;
|
||||
color: #31353D;
|
||||
}
|
||||
|
||||
image {
|
||||
width: 82rpx;
|
||||
height: 62rpx;
|
||||
vertical-align: middle;
|
||||
margin-left: 10rpx;
|
||||
}
|
||||
|
||||
.name {
|
||||
display: inline-block;
|
||||
width: 106rpx;
|
||||
font-size: 32rpx;
|
||||
font-family: PingFang SC-Regular, PingFang SC;
|
||||
font-weight: 400;
|
||||
color: #31353D;
|
||||
line-height: 38rpx;
|
||||
border-bottom: 2rpx solid #000000;
|
||||
margin-left: 16rpx;
|
||||
text-align: center;
|
||||
min-height: 38rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.price {
|
||||
width: 100%;
|
||||
font-size: 24rpx;
|
||||
font-family: PingFang SC-Regular, PingFang SC;
|
||||
font-weight: 400;
|
||||
color: #31353D;
|
||||
margin: 8rpx 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 温馨提示
|
||||
.my_prompt {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 0 4rpx;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
|
||||
.title {
|
||||
width: 100%;
|
||||
font-size: 28rpx;
|
||||
font-family: PingFang SC-Regular, PingFang SC;
|
||||
font-weight: 400;
|
||||
color: #A7ABB0;
|
||||
line-height: 40rpx;
|
||||
margin: 32rpx 0;
|
||||
}
|
||||
|
||||
image {
|
||||
width: 159rpx;
|
||||
height: 100rpx;
|
||||
position: absolute;
|
||||
left: 60%;
|
||||
top: 0%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
558
subPackages/sub_clinic_admin/order/detail.vue
Normal file
558
subPackages/sub_clinic_admin/order/detail.vue
Normal file
@@ -0,0 +1,558 @@
|
||||
<template>
|
||||
<clinic-page-layout title="订单详情" :show-back="true">
|
||||
<view class="detail-container">
|
||||
<!-- 头部面板:通栏,展示金额与状态 -->
|
||||
<view v-if="info.id" class="detail-panel header-panel">
|
||||
<view class="order-head">
|
||||
<text class="order-no-main">订单号:{{ info.order_no }}</text>
|
||||
<text class="pay-total">{{ money(info.total_pay_price) }}</text>
|
||||
</view>
|
||||
|
||||
<view class="tags">
|
||||
<text class="modern-tag tag-status">{{ detailExtra.statusText }}</text>
|
||||
<text class="modern-tag tag-type">{{ detailExtra.prescriptionTypeText }}</text>
|
||||
<text class="modern-tag tag-delivery">{{ detailExtra.deliveryText }}</text>
|
||||
</view>
|
||||
|
||||
<view class="sub-block">
|
||||
<view class="sub-block-title">基本信息</view>
|
||||
<view class="row"><text>订单类型</text><text>{{ detailExtra.orderTypeText }}</text></view>
|
||||
<view class="row"><text>患者</text><text>{{ info.patient || '-' }}</text></view>
|
||||
<view class="row"><text>医生</text><text>{{ doctorName }}</text></view>
|
||||
<view class="row"><text>处方来源</text><text>{{ storeName }}</text></view>
|
||||
<view class="row"><text>订单来源</text><text>{{ onlineText(info.is_online) }}</text></view>
|
||||
<view class="row"><text>ERP状态</text><text>{{ erpSyncText(info.is_sync_erp) }}</text></view>
|
||||
<view class="row"><text>支付时间</text><text>{{ info.pay_time || '-' }}</text></view>
|
||||
<view class="row"><text>下单时间</text><text>{{ info.created_at || '-' }}</text></view>
|
||||
<view class="row" v-if="detailExtra.cancelRemark"><text>发货备注</text><text>{{ detailExtra.cancelRemark }}</text></view>
|
||||
</view>
|
||||
|
||||
<view class="sub-block">
|
||||
<view class="sub-block-title">费用明细</view>
|
||||
<view class="row"><text>药品总价</text><text>{{ money(info.items_price) }}</text></view>
|
||||
<view class="row"><text>供货价</text><text>{{ detailExtra.marketPrice }}</text></view>
|
||||
<view class="row"><text>运费</text><text>{{ money(info.trans_expenses) }}</text></view>
|
||||
<view class="row"><text>挂号费</text><text>{{ money(info.register_price) }}</text></view>
|
||||
<view class="row"><text>是否包邮</text><text>{{ detailExtra.freeShippingText }}</text></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 配送信息面板 -->
|
||||
<view v-if="info.id" class="detail-panel">
|
||||
<view class="section-title">{{ receiverBlockTitle }}</view>
|
||||
<view class="row"><text>{{ receiverLabel }}</text><text>{{ receiverName }}</text></view>
|
||||
<view class="row"><text>联系电话</text><text>{{ receiverMobile }}</text></view>
|
||||
<view class="row" v-if="info.delivery_method === 0">
|
||||
<text>配送地址</text><text class="addr">{{ deliveryAddress }}</text>
|
||||
</view>
|
||||
<view class="row" v-else>
|
||||
<text>自提地址</text><text class="addr">{{ pickupAddress }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 商品列表面板 -->
|
||||
<view v-if="productItems.length" class="detail-panel">
|
||||
<view class="section-title">订单商品</view>
|
||||
|
||||
<!-- 如果是中药:高密度网格排列,一行多个,小且精致 -->
|
||||
<view v-if="isTcmOrder" class="tcm-grid-wrap">
|
||||
<view class="tcm-grid">
|
||||
<view v-for="(p, idx) in productDisplayList" :key="idx" class="herb-box">
|
||||
<text class="herb-name">{{ p.drugName }}</text>
|
||||
<text class="herb-qty">{{ p.qty }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 非中药:标准西药图文列表 -->
|
||||
<view v-else>
|
||||
<view
|
||||
v-for="(p, idx) in productDisplayList"
|
||||
:key="idx"
|
||||
class="product-item"
|
||||
:class="p.isTcm ? 'product-tcm' : 'product-west'"
|
||||
>
|
||||
<image
|
||||
v-if="!p.isTcm"
|
||||
class="product-img"
|
||||
:src="p.imageUrl"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view class="product-body">
|
||||
<view class="product-name">{{ p.drugName }}</view>
|
||||
<view class="product-meta">
|
||||
<text v-if="p.drugNumber">编号:{{ p.drugNumber }}</text>
|
||||
<text>数量:{{ p.qty }}</text>
|
||||
</view>
|
||||
<view class="product-line" v-if="!p.isTcm">规格:{{ p.specification }}</view>
|
||||
<view class="product-line" v-if="!p.isTcm">厂家:{{ p.manufacturer }}</view>
|
||||
|
||||
<view class="product-price-bar">
|
||||
<text class="price-main">单价:{{ p.price }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 处方信息内页入口 (保留作为备用) -->
|
||||
<view
|
||||
v-if="detailExtra.canViewPrescription"
|
||||
class="detail-panel card-link"
|
||||
hover-class="panel-hover"
|
||||
@click="openPrescription"
|
||||
>
|
||||
<view class="section-head">
|
||||
<text class="section-title">处方信息</text>
|
||||
<text class="section-arrow">查看完整处方 ➔</text>
|
||||
</view>
|
||||
<template v-if="detailExtra.showPrescription">
|
||||
<view class="row"><text>处方编号</text><text>{{ detailExtra.showPrescription.prescriptionNo }}</text></view>
|
||||
<view class="row"><text>诊断</text><text>{{ detailExtra.showPrescription.diagnose }}</text></view>
|
||||
<view class="row"><text>医嘱</text><text>{{ detailExtra.showPrescription.doctorOrder }}</text></view>
|
||||
<view class="row"><text>开药医生</text><text>{{ detailExtra.showPrescription.doctorName }}</text></view>
|
||||
</template>
|
||||
<view v-else class="prescription-hint">点击加载完整处方内容</view>
|
||||
</view>
|
||||
|
||||
<!-- 物流时间轴 -->
|
||||
<express-timeline
|
||||
v-if="info.id && info.delivery_method === 0"
|
||||
:express="expressInfo"
|
||||
:show="true"
|
||||
/>
|
||||
|
||||
<!-- 底部悬浮操作栏:移除确认发货,全宽查看处方 -->
|
||||
<view class="actions-bar" v-if="info.id && (canRefund || detailExtra.canViewPrescription)">
|
||||
<!-- 退款按钮为次要操作 -->
|
||||
<button v-if="canRefund" class="btn-warn" @click="refund">申请退款</button>
|
||||
<!-- 查看处方按钮为主操作,利用 flex: 1 自动铺满剩余宽度 -->
|
||||
<button v-if="detailExtra.canViewPrescription" class="btn-primary btn-full" @click="openPrescription">查看处方</button>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
<!-- 处方弹窗 -->
|
||||
<prescription-detail-popup ref="prescriptionPopup" />
|
||||
</clinic-page-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ClinicPageLayout from '../components/clinic-page-layout.vue'
|
||||
import ExpressTimeline from './components/ExpressTimeline.vue'
|
||||
import PrescriptionDetailPopup from './components/PrescriptionDetailPopup.vue'
|
||||
import {
|
||||
getOrderDetail,
|
||||
refundOrder,
|
||||
getExpressDetailByOrderId,
|
||||
} from '@/api/clinicAdmin.js'
|
||||
import {
|
||||
orderStatusText,
|
||||
deliveryMethodText,
|
||||
orderOnlineText,
|
||||
prescriptionTypeText,
|
||||
erpSyncText,
|
||||
mapOrderDetailDisplay,
|
||||
mapOrderProductItem,
|
||||
} from '../common/display.js'
|
||||
import { formatMoney } from '../common/format.js'
|
||||
|
||||
export default {
|
||||
components: { ClinicPageLayout, ExpressTimeline, PrescriptionDetailPopup },
|
||||
data() {
|
||||
return {
|
||||
id: 0,
|
||||
info: {},
|
||||
expressInfo: null,
|
||||
detailExtra: {},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
productItems() {
|
||||
return this.info.product_order_items || []
|
||||
},
|
||||
productDisplayList() {
|
||||
const pt = this.info.prescription_type
|
||||
return this.productItems.map(p => mapOrderProductItem(p, pt))
|
||||
},
|
||||
// 判断是否为中药处方,决定是否使用网格布局
|
||||
isTcmOrder() {
|
||||
return this.info.prescription_type === 1 || (this.productDisplayList.length > 0 && this.productDisplayList[0].isTcm);
|
||||
},
|
||||
doctorName() {
|
||||
const d = this.info.doctor
|
||||
return (d && d.name) ? d.name : '-'
|
||||
},
|
||||
storeName() {
|
||||
const s = this.info.store
|
||||
return (s && s.name) ? s.name : '-'
|
||||
},
|
||||
receiverBlockTitle() {
|
||||
return this.info.delivery_method === 0 ? '收货人信息' : '就诊人信息'
|
||||
},
|
||||
receiverLabel() {
|
||||
return this.info.delivery_method === 0 ? '收货人' : '就诊人'
|
||||
},
|
||||
receiverName() {
|
||||
if (this.info.delivery_method === 0) {
|
||||
const a = this.info.address
|
||||
return (a && a.name) ? a.name : '-'
|
||||
}
|
||||
return this.info.express_name || this.info.patient || '-'
|
||||
},
|
||||
receiverMobile() {
|
||||
const a = this.info.address
|
||||
if (a && a.mobile) return a.mobile
|
||||
return this.info.patient_mobile || '-'
|
||||
},
|
||||
deliveryAddress() {
|
||||
const a = this.info.address
|
||||
if (!a || typeof a !== 'object') return '-'
|
||||
return [a.province, a.region, a.detail_address].filter(Boolean).join(' ') || '-'
|
||||
},
|
||||
pickupAddress() {
|
||||
const s = this.info.store
|
||||
return (s && s.position) ? s.position : '-'
|
||||
},
|
||||
canRefund() {
|
||||
return this.info.status === 2 || this.info.status === 3
|
||||
}
|
||||
},
|
||||
onLoad(q) {
|
||||
this.id = Number(q.id || 0)
|
||||
this.load()
|
||||
},
|
||||
methods: {
|
||||
load() {
|
||||
getOrderDetail(this.id).then(w => {
|
||||
if (!w.ok) return
|
||||
this.info = w.data || {}
|
||||
this.detailExtra = mapOrderDetailDisplay(this.info)
|
||||
if (this.info.delivery_method === 0) {
|
||||
this.loadExpress()
|
||||
}
|
||||
})
|
||||
},
|
||||
loadExpress() {
|
||||
getExpressDetailByOrderId(this.id).then(w => {
|
||||
if (w.ok) this.expressInfo = w.data
|
||||
})
|
||||
},
|
||||
money(v) {
|
||||
return formatMoney(v)
|
||||
},
|
||||
statusText(s) {
|
||||
return orderStatusText(s)
|
||||
},
|
||||
deliveryText(dm) {
|
||||
return deliveryMethodText(dm)
|
||||
},
|
||||
onlineText(v) {
|
||||
return orderOnlineText(v)
|
||||
},
|
||||
prescriptionText(pt) {
|
||||
return prescriptionTypeText(pt)
|
||||
},
|
||||
erpSyncText(v) {
|
||||
return erpSyncText(v)
|
||||
},
|
||||
refund() {
|
||||
uni.showModal({
|
||||
title: '确认退款',
|
||||
confirmColor: '#F53F3F',
|
||||
success: (r) => {
|
||||
if (!r.confirm) return
|
||||
refundOrder({ id: this.id }).then(w => {
|
||||
if (w.ok) {
|
||||
uni.showToast({ title: '已提交' })
|
||||
this.load()
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
openPrescription() {
|
||||
const pid = this.detailExtra.prescriptionId
|
||||
if (!pid) return
|
||||
this.$refs.prescriptionPopup.open(pid)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$theme-primary: #6acdbb;
|
||||
$color-page-bg: #F5F7F8;
|
||||
$color-text-title: #222B2A;
|
||||
$color-text-body: #4E5969;
|
||||
$color-text-muted: #86909C;
|
||||
$color-border: #F2F3F5;
|
||||
$color-danger: #F53F3F;
|
||||
|
||||
.detail-container {
|
||||
background-color: $color-page-bg;
|
||||
min-height: 100vh;
|
||||
/* 适配底部操作栏,留出极大的安全边距 */
|
||||
padding-bottom: 180rpx;
|
||||
}
|
||||
|
||||
/* 通栏面板 */
|
||||
.detail-panel {
|
||||
background: #ffffff;
|
||||
padding: 32rpx 40rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.header-panel {
|
||||
padding-top: 40rpx;
|
||||
}
|
||||
|
||||
.order-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.order-no-main {
|
||||
font-size: 28rpx;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
.pay-total {
|
||||
font-size: 40rpx;
|
||||
color: $color-text-title;
|
||||
font-weight: 700;
|
||||
font-family: monospace, sans-serif;
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.modern-tag {
|
||||
font-size: 22rpx;
|
||||
padding: 6rpx 16rpx;
|
||||
border-radius: 8rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tag-status { background: rgba(106, 205, 187, 0.1); color: $theme-primary; }
|
||||
.tag-type { background: rgba(230, 162, 60, 0.1); color: #e6a23c; }
|
||||
.tag-delivery { background: rgba(91, 127, 214, 0.1); color: #5b7fd6; }
|
||||
|
||||
.sub-block {
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
|
||||
.sub-block-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: $color-text-title;
|
||||
margin-bottom: 8rpx;
|
||||
padding-bottom: 16rpx;
|
||||
border-bottom: 1rpx solid $color-border;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: $color-text-title;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.section-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.section-arrow {
|
||||
font-size: 26rpx;
|
||||
color: $theme-primary;
|
||||
}
|
||||
|
||||
.panel-hover {
|
||||
background-color: #FAFAFA;
|
||||
}
|
||||
|
||||
.prescription-hint {
|
||||
font-size: 26rpx;
|
||||
color: $color-text-muted;
|
||||
padding: 16rpx 0;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 20rpx 0;
|
||||
font-size: 28rpx;
|
||||
border-bottom: 1rpx dashed $color-border;
|
||||
gap: 24rpx;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
text:first-child {
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
text:last-child {
|
||||
color: $color-text-title;
|
||||
text-align: right;
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.addr { max-width: 460rpx; line-height: 1.4; }
|
||||
|
||||
/* ================= 专属中药网格区 ================= */
|
||||
.tcm-grid-wrap {
|
||||
margin-top: 24rpx;
|
||||
background: #F8FBFB;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.tcm-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr); /* 一排严格4个,精致小巧 */
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.herb-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #ffffff;
|
||||
border-radius: 12rpx;
|
||||
padding: 16rpx 8rpx;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.015);
|
||||
border: 1rpx solid #EAEFEF;
|
||||
}
|
||||
|
||||
.herb-name {
|
||||
font-size: 26rpx;
|
||||
color: $color-text-title;
|
||||
font-weight: 600;
|
||||
margin-bottom: 6rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.herb-qty {
|
||||
font-size: 24rpx;
|
||||
color: $theme-primary;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 商品信息 (西药模式) */
|
||||
.product-item {
|
||||
display: flex;
|
||||
padding: 32rpx 0;
|
||||
border-bottom: 1rpx solid $color-border;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.product-img {
|
||||
width: 140rpx;
|
||||
height: 140rpx;
|
||||
border-radius: 20rpx;
|
||||
flex-shrink: 0;
|
||||
background: $color-page-bg;
|
||||
border: 1rpx solid $color-border;
|
||||
}
|
||||
|
||||
.product-body {
|
||||
flex: 1;
|
||||
margin-left: 24rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-size: 30rpx;
|
||||
color: $color-text-title;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.product-meta {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
font-size: 24rpx;
|
||||
color: $color-text-muted;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.product-line {
|
||||
font-size: 24rpx;
|
||||
color: $color-text-muted;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
|
||||
.product-price-bar {
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.price-main { font-size: 28rpx; color: $color-text-title; font-weight: 600; }
|
||||
|
||||
/* ================= 底部动作栏 ================= */
|
||||
.actions-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #ffffff;
|
||||
padding: 24rpx 40rpx;
|
||||
/* 自动撑起 iOS 底部安全距离 */
|
||||
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
|
||||
box-shadow: 0 -4rpx 24rpx rgba(0,0,0,0.04);
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.actions-bar button {
|
||||
margin: 0;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
font-size: 30rpx;
|
||||
border-radius: 44rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.actions-bar button::after { display: none; }
|
||||
|
||||
/* 主按钮使用 flex: 1 铺满 */
|
||||
.btn-full {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: $theme-primary;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 次要操作(退款)给固定宽度 */
|
||||
.btn-warn {
|
||||
width: 220rpx;
|
||||
background: #fff;
|
||||
color: $color-danger;
|
||||
border: 2rpx solid $color-danger;
|
||||
}
|
||||
</style>
|
||||
582
subPackages/sub_clinic_admin/order/index.vue
Normal file
582
subPackages/sub_clinic_admin/order/index.vue
Normal file
@@ -0,0 +1,582 @@
|
||||
<template>
|
||||
<clinic-page-layout title="商品订单" :show-back="true">
|
||||
<view class="order-list-container">
|
||||
<!-- 顶部筛选 -->
|
||||
<page-sticky-header>
|
||||
<collapsible-filter-panel title="订单筛选" @clear="resetFilter">
|
||||
<view class="filter-row">
|
||||
<text class="filter-label">订单号</text>
|
||||
<input class="filter-input" v-model="orderNo" placeholder="输入订单号" placeholder-style="color:#BDBDBD" />
|
||||
</view>
|
||||
<view class="filter-row">
|
||||
<text class="filter-label">订单状态</text>
|
||||
<picker :range="statusLabels" @change="onStatus">
|
||||
<view class="filter-picker">{{ statusLabels[statusIndex] || '全部' }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="filter-row">
|
||||
<text class="filter-label">发货方式</text>
|
||||
<picker :range="deliveryLabels" @change="onDelivery">
|
||||
<view class="filter-picker">{{ deliveryLabels[deliveryIndex] }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="filter-row">
|
||||
<text class="filter-label">订单类型</text>
|
||||
<picker :range="prescriptionTypeLabels" @change="onPrescriptionType">
|
||||
<view class="filter-picker">{{ prescriptionTypeLabels[prescriptionTypeIndex] }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="filter-row date-row">
|
||||
<picker mode="date" :value="dateStart" @change="onStart">
|
||||
<view class="filter-picker date-picker">{{ dateStart || '开始日期' }}</view>
|
||||
</picker>
|
||||
<text class="sep">至</text>
|
||||
<picker mode="date" :value="dateEnd" @change="onEnd">
|
||||
<view class="filter-picker date-picker">{{ dateEnd || '结束日期' }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="filter-query-btn" @click="reload">查询</view>
|
||||
</collapsible-filter-panel>
|
||||
</page-sticky-header>
|
||||
|
||||
<!-- 统计卡片区 -->
|
||||
<scroll-view v-if="statItems.length" class="stats-scroll" scroll-x>
|
||||
<view class="stats">
|
||||
<view v-for="s in statItems" :key="s.key" class="stat-card">
|
||||
<text class="stat-label">{{ s.label }}</text>
|
||||
<text class="stat-value">{{ s.value }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 顶级悬浮卡片列表 -->
|
||||
<view class="card-list-wrap">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item._wxKey"
|
||||
class="premium-card"
|
||||
hover-class="card-hover"
|
||||
:hover-stay-time="150"
|
||||
@click="goDetail(item)"
|
||||
>
|
||||
<!-- 顶部:单号与状态 -->
|
||||
<view class="card-head">
|
||||
<view class="order-id">
|
||||
<text class="id-icon">单</text>
|
||||
<text class="id-text">{{ rowDisplay(item).orderNo }}</text>
|
||||
</view>
|
||||
<view
|
||||
class="status-badge"
|
||||
:class="['status-' + (rowDisplay(item).status || 'default')]"
|
||||
>
|
||||
{{ rowDisplay(item).statusText }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card-body">
|
||||
<!-- 核心视觉区:商品与金额 -->
|
||||
<view class="main-info">
|
||||
<view class="title-wrap">
|
||||
<text class="goods-title">{{ rowDisplay(item).productSummary || '药品订单' }}</text>
|
||||
<view class="goods-tags">
|
||||
<text class="modern-tag tag-type">{{ rowDisplay(item).prescriptionTypeText }}</text>
|
||||
<text class="modern-tag tag-delivery">{{ rowDisplay(item).deliveryText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="price-wrap">
|
||||
<text class="currency">¥</text>
|
||||
<text class="amount">{{ rowDisplay(item).totalPayPrice.replace('¥', '').trim() }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 信息岛 (Info Island):聚合次要信息的高级灰底区块 -->
|
||||
<view class="info-island">
|
||||
<view class="island-row" v-if="rowDisplay(item).userNickname !== '-'">
|
||||
<text class="island-label">就诊人</text>
|
||||
<text class="island-value">{{ rowDisplay(item).userNickname }}</text>
|
||||
</view>
|
||||
<view class="island-row">
|
||||
<text class="island-label">收件人</text>
|
||||
<text class="island-value">
|
||||
{{ rowDisplay(item).receiverName }}
|
||||
<text class="muted" v-if="rowDisplay(item).receiverMobile"> · {{ rowDisplay(item).receiverMobile }}</text>
|
||||
</text>
|
||||
</view>
|
||||
<view class="island-row" v-if="rowDisplay(item).storeName !== '-'">
|
||||
<text class="island-label">开方诊所</text>
|
||||
<text class="island-value">{{ rowDisplay(item).storeName }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部:时间与操作按键 -->
|
||||
<view class="card-foot">
|
||||
<text class="time">{{ rowDisplay(item).createdAt }}</text>
|
||||
<view class="actions">
|
||||
<view
|
||||
v-if="rowDisplay(item).canViewPrescription"
|
||||
class="btn-ghost"
|
||||
@click.stop="openPrescription(rowDisplay(item).pId)"
|
||||
>
|
||||
查看处方
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="!loading && !list.length" class="empty-state">
|
||||
<text>暂无订单记录</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<prescription-detail-popup ref="prescriptionPopup" />
|
||||
</clinic-page-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ClinicPageLayout from '../components/clinic-page-layout.vue'
|
||||
import PageStickyHeader from '../components/PageStickyHeader.vue'
|
||||
import CollapsibleFilterPanel from '../components/CollapsibleFilterPanel.vue'
|
||||
import PrescriptionDetailPopup from './components/PrescriptionDetailPopup.vue'
|
||||
import { getOrderList, getOrderStatusOption, getOrderSaleAmount } from '@/api/clinicAdmin.js'
|
||||
import { withWxKey } from '@/utils/wxListKey.js'
|
||||
import { mapOrderListRow } from '../common/display.js'
|
||||
import { formatMoney } from '../common/format.js'
|
||||
import {
|
||||
buildOrderListParams,
|
||||
buildOrderSaleAmountParams,
|
||||
} from '../common/orderParams.js'
|
||||
import {
|
||||
loadOrderFilter,
|
||||
saveOrderFilter,
|
||||
clearOrderFilter,
|
||||
defaultOrderFilter,
|
||||
} from '../common/filterCache.js'
|
||||
|
||||
const DELIVERY_OPTIONS = [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '快递到家', value: 0 },
|
||||
{ label: '诊所自提', value: 1 },
|
||||
]
|
||||
const PRESCRIPTION_TYPE_OPTIONS = [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '中药', value: 1 },
|
||||
{ label: '西药', value: 2 },
|
||||
{ label: '中成药', value: 3 },
|
||||
{ label: '产品服务包', value: 5 },
|
||||
]
|
||||
|
||||
export default {
|
||||
components: { ClinicPageLayout, PageStickyHeader, CollapsibleFilterPanel, PrescriptionDetailPopup },
|
||||
data() {
|
||||
return {
|
||||
list: [],
|
||||
page: 1,
|
||||
loading: false,
|
||||
orderNo: '',
|
||||
statusOptions: [{ label: '全部', value: '' }],
|
||||
statusIndex: 0,
|
||||
deliveryIndex: 0,
|
||||
prescriptionTypeIndex: 0,
|
||||
dateStart: '',
|
||||
dateEnd: '',
|
||||
statItems: [],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
statusLabels() {
|
||||
return this.statusOptions.map(s => s.label || s.name || '全部')
|
||||
},
|
||||
deliveryLabels() {
|
||||
return DELIVERY_OPTIONS.map(o => o.label)
|
||||
},
|
||||
prescriptionTypeLabels() {
|
||||
return PRESCRIPTION_TYPE_OPTIONS.map(o => o.label)
|
||||
},
|
||||
},
|
||||
onLoad() {
|
||||
this.applyOrderFilter(loadOrderFilter())
|
||||
getOrderStatusOption().then(w => {
|
||||
const opts = (w.data && (w.data.items || w.data)) || []
|
||||
if (Array.isArray(opts) && opts.length) {
|
||||
this.statusOptions = [{ label: '全部', value: '' }].concat(opts.map(o => ({
|
||||
label: o.label || o.name,
|
||||
value: o.value != null ? o.value : o.id,
|
||||
})))
|
||||
}
|
||||
})
|
||||
this.reload()
|
||||
},
|
||||
onReachBottom() {
|
||||
this.load(this.page + 1, true)
|
||||
},
|
||||
methods: {
|
||||
applyOrderFilter(f) {
|
||||
this.orderNo = f.orderNo
|
||||
this.statusIndex = f.statusIndex
|
||||
this.deliveryIndex = f.deliveryIndex
|
||||
this.prescriptionTypeIndex = f.prescriptionTypeIndex
|
||||
this.dateStart = f.dateStart
|
||||
this.dateEnd = f.dateEnd
|
||||
},
|
||||
persistOrderFilter() {
|
||||
saveOrderFilter({
|
||||
orderNo: this.orderNo,
|
||||
statusIndex: this.statusIndex,
|
||||
deliveryIndex: this.deliveryIndex,
|
||||
prescriptionTypeIndex: this.prescriptionTypeIndex,
|
||||
dateStart: this.dateStart,
|
||||
dateEnd: this.dateEnd,
|
||||
})
|
||||
},
|
||||
resetFilter() {
|
||||
this.applyOrderFilter(defaultOrderFilter())
|
||||
clearOrderFilter()
|
||||
this.reload()
|
||||
},
|
||||
filterValues() {
|
||||
const st = this.statusOptions[this.statusIndex]
|
||||
const del = DELIVERY_OPTIONS[this.deliveryIndex]
|
||||
const pt = PRESCRIPTION_TYPE_OPTIONS[this.prescriptionTypeIndex]
|
||||
return {
|
||||
orderNo: this.orderNo.trim(),
|
||||
status: st && st.value !== '' && st.value != null ? st.value : undefined,
|
||||
deliveryMethod: del.value !== '' ? del.value : undefined,
|
||||
prescriptionType: pt.value !== '' ? pt.value : undefined,
|
||||
dateStart: this.dateStart,
|
||||
dateEnd: this.dateEnd,
|
||||
}
|
||||
},
|
||||
rowDisplay(item) {
|
||||
return mapOrderListRow(item)
|
||||
},
|
||||
reload() {
|
||||
this.persistOrderFilter()
|
||||
this.page = 1
|
||||
this.load(1, false)
|
||||
this.loadSaleAmount()
|
||||
},
|
||||
loadSaleAmount() {
|
||||
const params = buildOrderSaleAmountParams(this.filterValues())
|
||||
getOrderSaleAmount(params).then(w => {
|
||||
if (!w.ok || !w.data) {
|
||||
this.statItems = []
|
||||
return
|
||||
}
|
||||
const d = w.data
|
||||
this.statItems = [
|
||||
{ key: 'total', label: '销售金额', value: formatMoney(d.total) },
|
||||
{ key: 'income', label: '我的收益', value: formatMoney(d.income) },
|
||||
]
|
||||
})
|
||||
},
|
||||
load(page, append) {
|
||||
this.loading = true
|
||||
const params = buildOrderListParams({
|
||||
page,
|
||||
pageSize: 15,
|
||||
...this.filterValues(),
|
||||
})
|
||||
getOrderList(params).then(w => {
|
||||
const items = withWxKey((w.data && w.data.items) || [], 'id', 'ord')
|
||||
this.list = append ? this.list.concat(items) : items
|
||||
this.page = page
|
||||
}).finally(() => { this.loading = false })
|
||||
},
|
||||
onStatus(e) {
|
||||
this.statusIndex = Number(e.detail.value)
|
||||
},
|
||||
onDelivery(e) {
|
||||
this.deliveryIndex = Number(e.detail.value)
|
||||
},
|
||||
onPrescriptionType(e) {
|
||||
this.prescriptionTypeIndex = Number(e.detail.value)
|
||||
},
|
||||
onStart(e) { this.dateStart = e.detail.value },
|
||||
onEnd(e) { this.dateEnd = e.detail.value },
|
||||
goDetail(item) {
|
||||
uni.navigateTo({ url: '/subPackages/sub_clinic_admin/order/detail?id=' + item.id })
|
||||
},
|
||||
openPrescription(pId) {
|
||||
if (!pId) return
|
||||
this.$refs.prescriptionPopup.open(pId)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$theme-primary: #6acdbb;
|
||||
$color-bg-base: #F2F4F7; /* Apple 风格的冷调高级灰底色 */
|
||||
$color-text-main: #1D2129;
|
||||
$color-text-body: #4E5969;
|
||||
$color-text-muted: #86909C;
|
||||
|
||||
.order-list-container {
|
||||
background-color: $color-bg-base;
|
||||
min-height: 100vh;
|
||||
padding-bottom: 60rpx;
|
||||
}
|
||||
|
||||
/* 筛选区 */
|
||||
.filter-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.filter-label {
|
||||
font-size: 28rpx;
|
||||
color: $color-text-body;
|
||||
min-width: 140rpx;
|
||||
}
|
||||
.filter-input, .filter-picker {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
padding: 16rpx 20rpx;
|
||||
background: #F7F8FA;
|
||||
border-radius: 12rpx;
|
||||
color: $color-text-main;
|
||||
}
|
||||
.date-row { flex-wrap: nowrap; }
|
||||
.date-picker { text-align: center; }
|
||||
.sep { color: $color-text-muted; margin: 0 8rpx; font-size: 24rpx; }
|
||||
.filter-query-btn {
|
||||
text-align: center;
|
||||
padding: 24rpx;
|
||||
background: $theme-primary;
|
||||
color: #fff;
|
||||
border-radius: 16rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
margin-top: 32rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(106, 205, 187, 0.2);
|
||||
}
|
||||
|
||||
/* 统计卡片 */
|
||||
.stats-scroll {
|
||||
margin-bottom: 20rpx;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.stats {
|
||||
display: inline-flex;
|
||||
gap: 20rpx;
|
||||
padding: 16rpx 32rpx;
|
||||
}
|
||||
.stat-card {
|
||||
display: inline-block;
|
||||
width: 320rpx;
|
||||
background: #fff;
|
||||
padding: 32rpx;
|
||||
border-radius: 24rpx;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.02);
|
||||
vertical-align: top;
|
||||
white-space: normal;
|
||||
}
|
||||
.stat-label {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
.stat-value {
|
||||
display: block;
|
||||
font-size: 42rpx;
|
||||
color: $color-text-main;
|
||||
font-weight: 700;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
/* ================= 核心高级卡片样式 ================= */
|
||||
.card-list-wrap {
|
||||
//padding: 0 24rpx;
|
||||
}
|
||||
|
||||
.premium-card {
|
||||
background: #ffffff;
|
||||
border-radius: 32rpx; /* 超大圆角带来前沿视觉享受 */
|
||||
margin-bottom: 24rpx;
|
||||
padding: 32rpx;
|
||||
border: 1rpx solid #E5E6EB;
|
||||
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.015); /* 极其克制的阴影 */
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.card-hover {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.01);
|
||||
background-color: #FAFAFA;
|
||||
}
|
||||
|
||||
/* 卡片头部 */
|
||||
.card-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 28rpx;
|
||||
}
|
||||
|
||||
.order-id {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.id-icon {
|
||||
font-size: 20rpx;
|
||||
color: #fff;
|
||||
background-color: #C9CDD4;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 6rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.id-text {
|
||||
font-size: 26rpx;
|
||||
color: $color-text-muted;
|
||||
font-family: SF Pro Text, monospace, sans-serif;
|
||||
letter-spacing: 0.5rpx;
|
||||
}
|
||||
|
||||
/* 状态标签配色优化 */
|
||||
.status-badge {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: $theme-primary;
|
||||
}
|
||||
/* 可根据实际的状态值(status)配置置灰 */
|
||||
.status-done { color: $color-text-muted; font-weight: 400; }
|
||||
.status-cancel { color: #F53F3F; }
|
||||
|
||||
/* 核心内容区 */
|
||||
.card-body {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.main-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 24rpx;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.title-wrap {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.goods-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: $color-text-main;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 16rpx;
|
||||
/* 多行截断 */
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.goods-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.modern-tag {
|
||||
font-size: 22rpx;
|
||||
padding: 4rpx 14rpx;
|
||||
border-radius: 8rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
.tag-type { background: #FFF3E8; color: #FF7D00; } /* 柔和爱马仕橙 */
|
||||
.tag-delivery { background: #E8F3FF; color: #165DFF; } /* 科技蓝 */
|
||||
|
||||
.price-wrap {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
color: $color-text-main;
|
||||
}
|
||||
|
||||
.currency {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
margin-right: 4rpx;
|
||||
}
|
||||
|
||||
.amount {
|
||||
font-size: 44rpx;
|
||||
font-weight: 700;
|
||||
font-family: DIN Alternate, SF Pro Display, sans-serif;
|
||||
}
|
||||
|
||||
/* 高级信息岛 (Inset Info Box) */
|
||||
.info-island {
|
||||
background-color: #F7F8FA;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.island-row {
|
||||
display: flex;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 12rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.island-label {
|
||||
color: $color-text-muted;
|
||||
width: 110rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.island-value {
|
||||
color: $color-text-body;
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: $color-text-muted;
|
||||
}
|
||||
}
|
||||
|
||||
/* 卡片底部 */
|
||||
.card-foot {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-top: 1rpx solid #F2F3F5;
|
||||
padding-top: 24rpx;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 24rpx;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
font-size: 26rpx;
|
||||
color: $theme-primary;
|
||||
background: rgba(106, 205, 187, 0.08);
|
||||
padding: 10rpx 32rpx;
|
||||
border-radius: 30rpx;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:active {
|
||||
background: rgba(106, 205, 187, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
color: $color-text-muted;
|
||||
padding: 120rpx 0;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
</style>
|
||||
799
subPackages/sub_clinic_admin/reconciliation/index.vue
Normal file
799
subPackages/sub_clinic_admin/reconciliation/index.vue
Normal file
@@ -0,0 +1,799 @@
|
||||
<template>
|
||||
<clinic-page-layout title="对账单" :show-back="true">
|
||||
<view class="reconciliation-container">
|
||||
<!-- 顶部吸顶区:Tabs 与 时间快筛 -->
|
||||
<page-sticky-header>
|
||||
<view class="header-section">
|
||||
<view class="tabs-wrap">
|
||||
<u-tabs
|
||||
:list="tabList"
|
||||
:is-scroll="false"
|
||||
:current="tabIndex"
|
||||
active-color="#6ACDBB"
|
||||
inactive-color="#86909C"
|
||||
bg-color="transparent"
|
||||
:bold="true"
|
||||
@change="onTabChange"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="quick-filter">
|
||||
<view class="date-range-picker">
|
||||
<picker mode="date" :value="dateStart" @change="onStart">
|
||||
<text class="date-text" :class="{'placeholder': !dateStart}">{{ dateStart || '开始日期' }}</text>
|
||||
</picker>
|
||||
<text class="sep">至</text>
|
||||
<picker mode="date" :value="dateEnd" @change="onEnd">
|
||||
<text class="date-text" :class="{'placeholder': !dateEnd}">{{ dateEnd || '结束日期' }}</text>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="filter-actions">
|
||||
<text class="btn-clear" @click="resetFilter">清空</text>
|
||||
<text class="btn-search" @click="reload">查询</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 药品折叠筛选面板 (保持逻辑不变,美化表单) -->
|
||||
<collapsible-filter-panel v-if="tab === 'drug'" title="药品高级筛选" @clear="resetFilter">
|
||||
<view class="filter-row switch-row">
|
||||
<view class="filter-label-group">
|
||||
<text class="filter-label">按订单维度拆分</text>
|
||||
<text class="filter-hint">未勾选时按药品+供货价合并</text>
|
||||
</view>
|
||||
<switch :checked="byOrder" color="#6acdbb" style="transform:scale(0.85)" @change="onByOrderChange" />
|
||||
</view>
|
||||
|
||||
<view class="filter-field">
|
||||
<text class="filter-label">药品</text>
|
||||
<view class="search-input-group">
|
||||
<input
|
||||
class="filter-input"
|
||||
v-model="drugSearchQ"
|
||||
placeholder="输入名称或拼音搜索"
|
||||
placeholder-style="color:#BDBDBD"
|
||||
confirm-type="search"
|
||||
@confirm="searchDrugOptions"
|
||||
/>
|
||||
<text class="filter-link" @click="searchDrugOptions">搜索</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="picker-wrap" v-if="drugOptionLabels.length">
|
||||
<picker :range="drugOptionLabels" @change="onDrugPick">
|
||||
<view class="filter-picker">{{ selectedDrugLabel || '点击选择匹配的药品' }}</view>
|
||||
</picker>
|
||||
<text v-if="drugId" class="filter-clear" @click="clearDrug">清除选择</text>
|
||||
</view>
|
||||
|
||||
<view class="filter-field">
|
||||
<text class="filter-label">订单</text>
|
||||
<view class="search-input-group">
|
||||
<input
|
||||
class="filter-input"
|
||||
v-model="orderSearchQ"
|
||||
placeholder="输入订单号搜索"
|
||||
placeholder-style="color:#BDBDBD"
|
||||
confirm-type="search"
|
||||
@confirm="searchOrderOptions"
|
||||
/>
|
||||
<text class="filter-link" @click="searchOrderOptions">搜索</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="picker-wrap" v-if="orderOptionLabels.length">
|
||||
<picker :range="orderOptionLabels" @change="onOrderPick">
|
||||
<view class="filter-picker">{{ selectedOrderLabel || '点击选择匹配的订单' }}</view>
|
||||
</picker>
|
||||
<text v-if="orderId" class="filter-clear" @click="clearOrder">清除选择</text>
|
||||
</view>
|
||||
|
||||
<view class="filter-query-btn" @click="reload">确认筛选</view>
|
||||
</collapsible-filter-panel>
|
||||
</page-sticky-header>
|
||||
|
||||
<!-- 统计卡片区:取消横向滑动,改为直观的网格平铺 -->
|
||||
<view v-if="statItems.length" class="stats-grid-wrap">
|
||||
<view class="stats-grid">
|
||||
<view v-for="s in statItems" :key="s.key" class="stat-card">
|
||||
<text class="stat-label">{{ s.label }}</text>
|
||||
<text class="stat-value">{{ s.value }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 数据列表区 -->
|
||||
<view class="list-wrap">
|
||||
<view v-for="item in list" :key="item._wxKey" class="premium-card">
|
||||
|
||||
<!-- ================= 诊所对账模式 ================= -->
|
||||
<template v-if="tab === 'store'">
|
||||
<view class="card-head">
|
||||
<text class="card-title">{{ rowStore(item).name }}</text>
|
||||
<text v-if="rowStore(item).registrationPrice !== '¥0.00'" class="mini-tag">挂号费 {{ rowStore(item).registrationPrice }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 核心摘要 -->
|
||||
<view class="hero-data">
|
||||
<view class="data-box">
|
||||
<text class="data-label">总销售额</text>
|
||||
<text class="data-val text-primary">{{ rowStore(item).totalSales }}</text>
|
||||
</view>
|
||||
<view class="data-divider"></view>
|
||||
<view class="data-box">
|
||||
<text class="data-label">总供货额</text>
|
||||
<text class="data-val">{{ rowStore(item).totalSupply }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 2x2 仪表盘网格:完美重构四大类费用 -->
|
||||
<view class="dashboard-grid">
|
||||
<!-- 草药 -->
|
||||
<view class="dashboard-island">
|
||||
<view class="island-head">
|
||||
<text class="island-title">🌿 草药</text>
|
||||
<text class="island-sales">销量 {{ rowStore(item).herbal.sales }}</text>
|
||||
</view>
|
||||
<view class="island-line"><text>销售</text><text>{{ rowStore(item).herbal.salesPrice }}</text></view>
|
||||
<view class="island-line"><text>供货</text><text>{{ rowStore(item).herbal.supplyPrice }}</text></view>
|
||||
</view>
|
||||
<!-- 西药 -->
|
||||
<view class="dashboard-island">
|
||||
<view class="island-head">
|
||||
<text class="island-title">💊 西药</text>
|
||||
<text class="island-sales">销量 {{ rowStore(item).medicine.sales }}</text>
|
||||
</view>
|
||||
<view class="island-line"><text>销售</text><text>{{ rowStore(item).medicine.salesPrice }}</text></view>
|
||||
<view class="island-line"><text>供货</text><text>{{ rowStore(item).medicine.supplyPrice }}</text></view>
|
||||
</view>
|
||||
<!-- 服务包 -->
|
||||
<view class="dashboard-island">
|
||||
<view class="island-head">
|
||||
<text class="island-title">📦 服务包</text>
|
||||
<text class="island-sales">销量 {{ rowStore(item).servicePackage.sales }}</text>
|
||||
</view>
|
||||
<view class="island-line"><text>销售</text><text>{{ rowStore(item).servicePackage.salesPrice }}</text></view>
|
||||
<view class="island-line"><text>供货</text><text>{{ rowStore(item).servicePackage.supplyPrice }}</text></view>
|
||||
</view>
|
||||
<!-- 其他费用 -->
|
||||
<view class="dashboard-island">
|
||||
<view class="island-head">
|
||||
<text class="island-title">🔖 其他费用</text>
|
||||
</view>
|
||||
<view class="island-line"><text>诊疗费</text><text>{{ rowStore(item).otherFees.treatment }}</text></view>
|
||||
<view class="island-line"><text>加工费</text><text>{{ rowStore(item).otherFees.process }}</text></view>
|
||||
<view class="island-line"><text>快递费</text><text>{{ rowStore(item).otherFees.express }}</text></view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- ================= 药品对账模式 ================= -->
|
||||
<template v-else>
|
||||
<view class="card-head">
|
||||
<text class="card-title">{{ rowDrug(item).drugName }}</text>
|
||||
<view class="drug-meta" v-if="rowDrug(item).drugNumber">
|
||||
<text class="mini-tag">编号 {{ rowDrug(item).drugNumber }}</text>
|
||||
<text class="mini-tag gray">{{ rowDrug(item).typeText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="info-island">
|
||||
<view class="island-row">
|
||||
<text class="island-label">销量</text>
|
||||
<text class="island-value text-primary font-bold">{{ rowDrug(item).number }}</text>
|
||||
</view>
|
||||
<view class="island-row">
|
||||
<text class="island-label">供货单价</text>
|
||||
<text class="island-value">{{ rowDrug(item).marketPrice }}</text>
|
||||
</view>
|
||||
<view class="island-row">
|
||||
<text class="island-label">总销售额</text>
|
||||
<text class="island-value">{{ rowDrug(item).totalSales }}</text>
|
||||
</view>
|
||||
<view class="island-row">
|
||||
<text class="island-label">总供货额</text>
|
||||
<text class="island-value">{{ rowDrug(item).totalSupply }}</text>
|
||||
</view>
|
||||
<view class="island-row" v-if="rowDrug(item).orderNo">
|
||||
<text class="island-label">订单号</text>
|
||||
<text class="island-value font-mono">{{ rowDrug(item).orderNo }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="!loading && !list.length" class="empty-state">
|
||||
<text>暂无对账数据</text>
|
||||
</view>
|
||||
</view>
|
||||
</clinic-page-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ClinicPageLayout from '../components/clinic-page-layout.vue'
|
||||
import PageStickyHeader from '../components/PageStickyHeader.vue'
|
||||
import CollapsibleFilterPanel from '../components/CollapsibleFilterPanel.vue'
|
||||
import {
|
||||
getReconciliationList,
|
||||
getReconciliationDrugList,
|
||||
getReconciliationDrugOptions,
|
||||
getReconciliationOrderOptions,
|
||||
} from '@/api/clinicAdmin.js'
|
||||
import { withWxKey } from '@/utils/wxListKey.js'
|
||||
import {
|
||||
mapReconciliationStoreRow,
|
||||
mapReconciliationDrugRow,
|
||||
mapReconciliationStoreCount,
|
||||
mapReconciliationDrugCount,
|
||||
} from '../common/display.js'
|
||||
import {
|
||||
buildStoreListParams,
|
||||
buildDrugListParams,
|
||||
buildSearchTimeQueryParams,
|
||||
normalizeReconciliationResponse,
|
||||
} from '../common/reconciliationParams.js'
|
||||
import {
|
||||
loadReconciliationFilter,
|
||||
saveReconciliationFilter,
|
||||
clearReconciliationFilter,
|
||||
defaultReconciliationFilter,
|
||||
} from '../common/filterCache.js'
|
||||
|
||||
export default {
|
||||
components: { ClinicPageLayout, PageStickyHeader, CollapsibleFilterPanel },
|
||||
data() {
|
||||
return {
|
||||
tab: 'store',
|
||||
tabIndex: 0,
|
||||
tabList: [{ name: '诊所对账' }, { name: '药品对账' }],
|
||||
dateStart: '',
|
||||
dateEnd: '',
|
||||
list: [],
|
||||
loading: false,
|
||||
statItems: [],
|
||||
byOrder: false,
|
||||
drugSearchQ: '',
|
||||
orderSearchQ: '',
|
||||
drugOptions: [],
|
||||
orderOptions: [],
|
||||
drugId: null,
|
||||
orderId: null,
|
||||
selectedDrugLabel: '',
|
||||
selectedOrderLabel: '',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
drugOptionLabels() {
|
||||
return this.drugOptions.map(o => o.label)
|
||||
},
|
||||
orderOptionLabels() {
|
||||
return this.orderOptions.map(o => o.label)
|
||||
},
|
||||
},
|
||||
onLoad() {
|
||||
this.applyReconciliationFilter(loadReconciliationFilter())
|
||||
this.reload()
|
||||
},
|
||||
methods: {
|
||||
applyReconciliationFilter(f) {
|
||||
this.tabIndex = f.tabIndex
|
||||
this.tab = f.tabIndex === 1 ? 'drug' : 'store'
|
||||
this.dateStart = f.dateStart
|
||||
this.dateEnd = f.dateEnd
|
||||
this.byOrder = f.byOrder
|
||||
this.drugSearchQ = f.drugSearchQ
|
||||
this.orderSearchQ = f.orderSearchQ
|
||||
this.drugId = f.drugId
|
||||
this.orderId = f.orderId
|
||||
this.selectedDrugLabel = f.selectedDrugLabel
|
||||
this.selectedOrderLabel = f.selectedOrderLabel
|
||||
this.drugOptions = []
|
||||
this.orderOptions = []
|
||||
},
|
||||
persistReconciliationFilter() {
|
||||
saveReconciliationFilter({
|
||||
tabIndex: this.tabIndex,
|
||||
dateStart: this.dateStart,
|
||||
dateEnd: this.dateEnd,
|
||||
byOrder: this.byOrder,
|
||||
drugSearchQ: this.drugSearchQ,
|
||||
orderSearchQ: this.orderSearchQ,
|
||||
drugId: this.drugId,
|
||||
orderId: this.orderId,
|
||||
selectedDrugLabel: this.selectedDrugLabel,
|
||||
selectedOrderLabel: this.selectedOrderLabel,
|
||||
})
|
||||
},
|
||||
resetFilter() {
|
||||
this.applyReconciliationFilter(defaultReconciliationFilter())
|
||||
clearReconciliationFilter()
|
||||
this.reload()
|
||||
},
|
||||
rowStore(item) {
|
||||
return mapReconciliationStoreRow(item)
|
||||
},
|
||||
rowDrug(item) {
|
||||
return mapReconciliationDrugRow(item)
|
||||
},
|
||||
onTabChange(index) {
|
||||
this.tabIndex = index
|
||||
this.tab = index === 0 ? 'store' : 'drug'
|
||||
this.reload()
|
||||
},
|
||||
reload() {
|
||||
this.persistReconciliationFilter()
|
||||
this.load()
|
||||
},
|
||||
load() {
|
||||
this.loading = true
|
||||
const isDrug = this.tab === 'drug'
|
||||
const api = isDrug ? getReconciliationDrugList : getReconciliationList
|
||||
const params = isDrug
|
||||
? buildDrugListParams({
|
||||
dateStart: this.dateStart,
|
||||
dateEnd: this.dateEnd,
|
||||
drugId: this.drugId,
|
||||
orderId: this.orderId,
|
||||
byOrder: this.byOrder,
|
||||
})
|
||||
: buildStoreListParams({
|
||||
dateStart: this.dateStart,
|
||||
dateEnd: this.dateEnd,
|
||||
excludeZeroSales: 0,
|
||||
})
|
||||
api(params).then(w => {
|
||||
const { count, items } = normalizeReconciliationResponse(w.data)
|
||||
this.statItems = isDrug
|
||||
? mapReconciliationDrugCount(count)
|
||||
: mapReconciliationStoreCount(count)
|
||||
const keyField = isDrug ? 'drug_id' : 'id'
|
||||
this.list = withWxKey(items, keyField, 'rec')
|
||||
}).finally(() => { this.loading = false })
|
||||
},
|
||||
onStart(e) { this.dateStart = e.detail.value },
|
||||
onEnd(e) { this.dateEnd = e.detail.value },
|
||||
onByOrderChange(e) {
|
||||
this.byOrder = !!e.detail.value
|
||||
},
|
||||
timeParams() {
|
||||
return buildSearchTimeQueryParams(this.dateStart, this.dateEnd)
|
||||
},
|
||||
searchDrugOptions() {
|
||||
if (!this.dateStart || !this.dateEnd) {
|
||||
uni.showToast({ title: '请选择时间范围', icon: 'none' })
|
||||
return
|
||||
}
|
||||
getReconciliationDrugOptions({
|
||||
q: this.drugSearchQ,
|
||||
limit: 20,
|
||||
...this.timeParams(),
|
||||
}).then(w => {
|
||||
const items = (w.data && w.data.items) || []
|
||||
this.drugOptions = items
|
||||
if (!items.length) uni.showToast({ title: '无匹配药品', icon: 'none' })
|
||||
})
|
||||
},
|
||||
searchOrderOptions() {
|
||||
if (!this.dateStart || !this.dateEnd) {
|
||||
uni.showToast({ title: '请选择时间范围', icon: 'none' })
|
||||
return
|
||||
}
|
||||
getReconciliationOrderOptions({
|
||||
q: this.orderSearchQ,
|
||||
limit: 20,
|
||||
...this.timeParams(),
|
||||
}).then(w => {
|
||||
const items = (w.data && w.data.items) || []
|
||||
this.orderOptions = items
|
||||
if (!items.length) uni.showToast({ title: '无匹配订单', icon: 'none' })
|
||||
})
|
||||
},
|
||||
onDrugPick(e) {
|
||||
const idx = Number(e.detail.value)
|
||||
const opt = this.drugOptions[idx]
|
||||
if (opt) {
|
||||
this.drugId = opt.value
|
||||
this.selectedDrugLabel = opt.label
|
||||
}
|
||||
},
|
||||
onOrderPick(e) {
|
||||
const idx = Number(e.detail.value)
|
||||
const opt = this.orderOptions[idx]
|
||||
if (opt) {
|
||||
this.orderId = opt.value
|
||||
this.selectedOrderLabel = opt.label
|
||||
}
|
||||
},
|
||||
clearDrug() {
|
||||
this.drugId = null
|
||||
this.selectedDrugLabel = ''
|
||||
this.drugOptions = []
|
||||
},
|
||||
clearOrder() {
|
||||
this.orderId = null
|
||||
this.selectedOrderLabel = ''
|
||||
this.orderOptions = []
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$theme-primary: #6acdbb;
|
||||
$color-bg-base: #F5F7F8;
|
||||
$color-text-main: #1D2129;
|
||||
$color-text-body: #4E5969;
|
||||
$color-text-muted: #86909C;
|
||||
$color-border: #E5E6EB;
|
||||
|
||||
.reconciliation-container {
|
||||
background-color: $color-bg-base;
|
||||
min-height: 100vh;
|
||||
padding-bottom: 60rpx;
|
||||
}
|
||||
|
||||
/* 顶部吸顶区 */
|
||||
.header-section {
|
||||
background: #ffffff;
|
||||
padding-bottom: 24rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.02);
|
||||
}
|
||||
|
||||
.tabs-wrap {
|
||||
border-bottom: 1rpx solid $color-border;
|
||||
}
|
||||
|
||||
.quick-filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx 32rpx 0;
|
||||
}
|
||||
|
||||
.date-range-picker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #F7F8FA;
|
||||
border-radius: 12rpx;
|
||||
padding: 12rpx 24rpx;
|
||||
flex: 1;
|
||||
margin-right: 24rpx;
|
||||
}
|
||||
|
||||
.date-text {
|
||||
font-size: 26rpx;
|
||||
color: $color-text-main;
|
||||
font-weight: 500;
|
||||
|
||||
&.placeholder {
|
||||
color: #BDBDBD;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
.sep {
|
||||
color: $color-text-muted;
|
||||
font-size: 24rpx;
|
||||
margin: 0 16rpx;
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.btn-clear {
|
||||
font-size: 26rpx;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
.btn-search {
|
||||
font-size: 26rpx;
|
||||
color: #fff;
|
||||
background: $theme-primary;
|
||||
padding: 10rpx 24rpx;
|
||||
border-radius: 30rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 药品筛选展开面板优化 */
|
||||
.switch-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.filter-label-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-size: 28rpx;
|
||||
color: $color-text-main;
|
||||
font-weight: 500;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.filter-hint {
|
||||
font-size: 22rpx;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
.filter-field {
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.search-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #F7F8FA;
|
||||
border-radius: 12rpx;
|
||||
padding: 8rpx 8rpx 8rpx 24rpx;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
flex: 1;
|
||||
font-size: 26rpx;
|
||||
color: $color-text-main;
|
||||
height: 60rpx;
|
||||
}
|
||||
|
||||
.filter-link {
|
||||
font-size: 26rpx;
|
||||
color: $theme-primary;
|
||||
padding: 12rpx 24rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.picker-wrap {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.filter-picker {
|
||||
font-size: 26rpx;
|
||||
color: $color-text-main;
|
||||
padding: 20rpx 24rpx;
|
||||
background: #F7F8FA;
|
||||
border-radius: 12rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.filter-clear {
|
||||
font-size: 24rpx;
|
||||
color: #F53F3F;
|
||||
display: block;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.filter-query-btn {
|
||||
text-align: center;
|
||||
padding: 24rpx;
|
||||
background: $theme-primary;
|
||||
color: #fff;
|
||||
border-radius: 16rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
margin-top: 32rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(106, 205, 187, 0.2);
|
||||
}
|
||||
|
||||
/* ================= 统计卡片平铺网格区 ================= */
|
||||
.stats-grid-wrap {
|
||||
padding: 24rpx 24rpx 8rpx; /* 控制和卡片之间的呼吸感 */
|
||||
}
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr); /* 取消了scroll-x滚动,改成直接平铺两列 */
|
||||
gap: 20rpx;
|
||||
}
|
||||
.stat-card {
|
||||
background: #fff;
|
||||
padding: 32rpx 24rpx;
|
||||
border-radius: 20rpx;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.02);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
.stat-label {
|
||||
font-size: 24rpx;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
.stat-value {
|
||||
font-size: 36rpx;
|
||||
color: $color-text-main;
|
||||
font-weight: 700;
|
||||
margin-top: 12rpx;
|
||||
font-family: DIN Alternate, monospace, sans-serif;
|
||||
/* 添加截断处理防止极端数字导致错位 */
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* ================= 核心高级卡片 ================= */
|
||||
.list-wrap {
|
||||
padding: 0 24rpx;
|
||||
}
|
||||
|
||||
.premium-card {
|
||||
background: #ffffff;
|
||||
border-radius: 24rpx;
|
||||
padding: 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
border: 1rpx solid #E5E6EB;
|
||||
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.015);
|
||||
}
|
||||
|
||||
.card-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 32rpx;
|
||||
color: $color-text-main;
|
||||
font-weight: 600;
|
||||
flex: 1;
|
||||
line-height: 1.4;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.mini-tag {
|
||||
font-size: 20rpx;
|
||||
padding: 4rpx 12rpx;
|
||||
background: rgba(106, 205, 187, 0.1);
|
||||
color: $theme-primary;
|
||||
border-radius: 8rpx;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
|
||||
&.gray {
|
||||
background: #F2F3F5;
|
||||
color: $color-text-body;
|
||||
margin-left: 12rpx;
|
||||
}
|
||||
}
|
||||
|
||||
/* 诊所对账:核心摘要 */
|
||||
.hero-data {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #F8FBFB;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.data-box {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.data-divider {
|
||||
width: 2rpx;
|
||||
height: 60rpx;
|
||||
background: #EAEFEF;
|
||||
margin: 0 32rpx;
|
||||
}
|
||||
|
||||
.data-label {
|
||||
font-size: 24rpx;
|
||||
color: $color-text-muted;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.data-val {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: $color-text-main;
|
||||
font-family: DIN Alternate, monospace, sans-serif;
|
||||
|
||||
&.text-primary {
|
||||
color: $theme-primary;
|
||||
}
|
||||
}
|
||||
|
||||
/* 诊所对账:2x2 仪表盘网格 */
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.dashboard-island {
|
||||
background: #F7F8FA;
|
||||
border-radius: 16rpx;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.island-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
padding-bottom: 12rpx;
|
||||
border-bottom: 1rpx dashed #E5E6EB;
|
||||
}
|
||||
|
||||
.island-title {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: $color-text-main;
|
||||
}
|
||||
|
||||
.island-sales {
|
||||
font-size: 22rpx;
|
||||
color: $theme-primary;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.island-line {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 24rpx;
|
||||
color: $color-text-body;
|
||||
margin-bottom: 8rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
text:last-child {
|
||||
font-family: monospace, sans-serif;
|
||||
color: $color-text-main;
|
||||
}
|
||||
}
|
||||
|
||||
/* 药品对账:通用信息岛 */
|
||||
.drug-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.info-island {
|
||||
background: #F7F8FA;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.island-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 26rpx;
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.island-label {
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
.island-value {
|
||||
color: $color-text-main;
|
||||
|
||||
&.text-primary { color: $theme-primary; }
|
||||
&.font-bold { font-weight: 600; font-size: 28rpx; }
|
||||
&.font-mono { font-family: SF Pro Text, monospace, sans-serif; font-size: 24rpx; color: $color-text-muted; }
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
color: $color-text-muted;
|
||||
padding: 120rpx 0;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,130 @@
|
||||
<template>
|
||||
<u-popup v-model="show" mode="center" border-radius="16" width="85%" :closeable="true" @close="onClose">
|
||||
<view class="modal">
|
||||
<view class="modal-title">修改售价</view>
|
||||
<view v-if="drugName" class="modal-drug">{{ drugName }}</view>
|
||||
<view v-if="suggestPrice" class="modal-hint">建议售价 {{ suggestPrice }}</view>
|
||||
<input
|
||||
class="modal-input"
|
||||
type="digit"
|
||||
v-model="priceInput"
|
||||
placeholder="请输入新售价"
|
||||
/>
|
||||
<view class="modal-actions">
|
||||
<button class="btn cancel" @click="onClose">取消</button>
|
||||
<button class="btn confirm" :loading="submitting" @click="onConfirm">确定</button>
|
||||
</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'PriceEditModal',
|
||||
props: {
|
||||
visible: { type: Boolean, default: false },
|
||||
item: { type: Object, default: null },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
show: false,
|
||||
priceInput: '',
|
||||
submitting: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
drugName() {
|
||||
const drug = (this.item && this.item.drug) || {}
|
||||
return drug.drug_name || ''
|
||||
},
|
||||
suggestPrice() {
|
||||
if (!this.item) return ''
|
||||
const drug = this.item.drug || {}
|
||||
const storeDrug = drug.drug_store_drug || {}
|
||||
const p = storeDrug.price != null ? storeDrug.price : drug.price
|
||||
if (p == null || p === '') return ''
|
||||
return '¥' + Number(p).toFixed(2)
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
visible: {
|
||||
immediate: true,
|
||||
handler(v) {
|
||||
this.show = v
|
||||
if (v && this.item) {
|
||||
const raw = this.item.price
|
||||
this.priceInput = raw != null && raw !== '' ? String(raw) : ''
|
||||
}
|
||||
},
|
||||
},
|
||||
show(v) {
|
||||
if (!v) this.$emit('close')
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onClose() {
|
||||
this.show = false
|
||||
this.$emit('close')
|
||||
},
|
||||
onConfirm() {
|
||||
const price = parseFloat(String(this.priceInput).trim())
|
||||
if (!price || price <= 0 || Number.isNaN(price)) {
|
||||
uni.showToast({ title: '请输入有效售价', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const rounded = Math.round(price * 100) / 100
|
||||
this.$emit('confirm', rounded)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.modal {
|
||||
padding: 40rpx 32rpx 32rpx;
|
||||
}
|
||||
.modal-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
}
|
||||
.modal-drug {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
margin-top: 16rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.modal-hint {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-top: 8rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.modal-input {
|
||||
margin-top: 32rpx;
|
||||
padding: 20rpx 24rpx;
|
||||
background: #f5f5f5;
|
||||
border-radius: 12rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
margin-top: 40rpx;
|
||||
}
|
||||
.btn {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
border-radius: 44rpx;
|
||||
margin: 0;
|
||||
}
|
||||
.btn.cancel {
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
}
|
||||
.btn.confirm {
|
||||
background: #6acdbb;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
135
subPackages/sub_clinic_admin/warehouse/detail.vue
Normal file
135
subPackages/sub_clinic_admin/warehouse/detail.vue
Normal file
@@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<clinic-page-layout title="药品详情" :show-back="true">
|
||||
<view v-if="info.id" class="card">
|
||||
<image class="hero" :src="display.imageUrl" mode="aspectFit" />
|
||||
<view class="row"><text>药品名称</text><text>{{ display.drugName }}</text></view>
|
||||
<view class="row"><text>拼音</text><text>{{ display.pinyin }}</text></view>
|
||||
<view class="row"><text>供货价</text><text>{{ display.buyPrice }}</text></view>
|
||||
<view class="row"><text>建议售价</text><text>{{ display.suggestPrice }}</text></view>
|
||||
<view class="row"><text>售价</text><text>{{ display.price }}</text></view>
|
||||
<view class="row"><text>库存</text><text>{{ display.stock }}</text></view>
|
||||
<view class="row"><text>状态</text><text>{{ display.statusText }}</text></view>
|
||||
</view>
|
||||
|
||||
<view v-if="info.id" class="btn-group">
|
||||
<button class="btn secondary" @click="openPriceEdit">修改售价</button>
|
||||
<button class="btn primary" @click="toggleStatus">
|
||||
{{ display.isOnShelf ? '下架' : '上架' }}
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<price-edit-modal
|
||||
:visible="priceModalVisible"
|
||||
:item="info.id ? info : null"
|
||||
@close="closePriceEdit"
|
||||
@confirm="onPriceConfirm"
|
||||
/>
|
||||
</clinic-page-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ClinicPageLayout from '../components/clinic-page-layout.vue'
|
||||
import PriceEditModal from './components/PriceEditModal.vue'
|
||||
import {
|
||||
getWarehouseDetail,
|
||||
updateWarehouseStatus,
|
||||
updateWarehousePrice,
|
||||
} from '@/api/clinicAdmin.js'
|
||||
import { mapWarehouseRow } from '../common/display.js'
|
||||
|
||||
export default {
|
||||
components: { ClinicPageLayout, PriceEditModal },
|
||||
data() {
|
||||
return {
|
||||
id: 0,
|
||||
info: {},
|
||||
priceModalVisible: false,
|
||||
priceSubmitting: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
display() {
|
||||
return mapWarehouseRow(this.info)
|
||||
},
|
||||
},
|
||||
onLoad(q) {
|
||||
this.id = Number(q.id || 0)
|
||||
this.load()
|
||||
},
|
||||
methods: {
|
||||
load() {
|
||||
getWarehouseDetail(this.id).then(w => {
|
||||
if (w.ok) this.info = w.data || {}
|
||||
})
|
||||
},
|
||||
openPriceEdit() {
|
||||
this.priceModalVisible = true
|
||||
},
|
||||
closePriceEdit() {
|
||||
this.priceModalVisible = false
|
||||
},
|
||||
onPriceConfirm(price) {
|
||||
if (this.priceSubmitting) return
|
||||
this.priceSubmitting = true
|
||||
updateWarehousePrice({ id: this.id, price }).then(w => {
|
||||
if (w.ok) {
|
||||
uni.showToast({ title: '修改成功' })
|
||||
this.closePriceEdit()
|
||||
this.load()
|
||||
}
|
||||
}).finally(() => { this.priceSubmitting = false })
|
||||
},
|
||||
toggleStatus() {
|
||||
updateWarehouseStatus(this.id).then(w => {
|
||||
if (w.ok) {
|
||||
uni.showToast({ title: '操作成功' })
|
||||
this.load()
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 32rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
.hero {
|
||||
width: 100%;
|
||||
height: 360rpx;
|
||||
margin-bottom: 24rpx;
|
||||
border-radius: 12rpx;
|
||||
background: #f8f8f8;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 16rpx 0;
|
||||
font-size: 28rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
.btn-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
}
|
||||
.btn {
|
||||
border-radius: 44rpx;
|
||||
font-size: 30rpx;
|
||||
margin: 0;
|
||||
}
|
||||
.btn.primary {
|
||||
background: #6acdbb;
|
||||
color: #fff;
|
||||
}
|
||||
.btn.secondary {
|
||||
background: #fff;
|
||||
color: #6acdbb;
|
||||
border: 1rpx solid #6acdbb;
|
||||
}
|
||||
</style>
|
||||
231
subPackages/sub_clinic_admin/warehouse/index.vue
Normal file
231
subPackages/sub_clinic_admin/warehouse/index.vue
Normal file
@@ -0,0 +1,231 @@
|
||||
<template>
|
||||
<clinic-page-layout :title="pageTitle" :show-back="true">
|
||||
<view class="search">
|
||||
<input v-model="keyword" placeholder="搜索药品名称" @confirm="reload" />
|
||||
<text @click="reload">搜索</text>
|
||||
</view>
|
||||
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item._wxKey"
|
||||
class="item"
|
||||
@click="goDetail(item)"
|
||||
>
|
||||
<image class="thumb" :src="rowDisplay(item).imageUrl" mode="aspectFill" />
|
||||
<view class="body">
|
||||
<view class="title">{{ rowDisplay(item).drugName }}</view>
|
||||
<view class="sub">供货 {{ rowDisplay(item).buyPrice }} · 售价 {{ rowDisplay(item).price }}</view>
|
||||
<view class="sub">库存 {{ rowDisplay(item).stock }}</view>
|
||||
</view>
|
||||
<view class="actions" @click.stop>
|
||||
<view class="tag" :class="rowDisplay(item).isOnShelf ? 'on' : 'off'">{{ rowDisplay(item).statusText }}</view>
|
||||
<view class="btn-row">
|
||||
<text class="act-btn" @click="openPriceEdit(item)">改价</text>
|
||||
<text class="act-btn shelf" @click="toggleStatus(item)">
|
||||
{{ rowDisplay(item).isOnShelf ? '下架' : '上架' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="!loading && !list.length" class="empty">暂无药品</view>
|
||||
|
||||
<price-edit-modal
|
||||
:visible="priceModalVisible"
|
||||
:item="priceEditItem"
|
||||
@close="closePriceEdit"
|
||||
@confirm="onPriceConfirm"
|
||||
/>
|
||||
</clinic-page-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ClinicPageLayout from '../components/clinic-page-layout.vue'
|
||||
import PriceEditModal from './components/PriceEditModal.vue'
|
||||
import {
|
||||
getWarehouseList,
|
||||
updateWarehouseStatus,
|
||||
updateWarehousePrice,
|
||||
} from '@/api/clinicAdmin.js'
|
||||
import { withWxKey } from '@/utils/wxListKey.js'
|
||||
import { mapWarehouseRow } from '../common/display.js'
|
||||
|
||||
export default {
|
||||
components: { ClinicPageLayout, PriceEditModal },
|
||||
data() {
|
||||
return {
|
||||
pageTitle: '仓库管理',
|
||||
warehouseType: '',
|
||||
keyword: '',
|
||||
list: [],
|
||||
page: 1,
|
||||
loading: false,
|
||||
priceModalVisible: false,
|
||||
priceEditItem: null,
|
||||
priceSubmitting: false,
|
||||
}
|
||||
},
|
||||
onLoad(q) {
|
||||
if (q.label) {
|
||||
this.pageTitle = decodeURIComponent(q.label)
|
||||
}
|
||||
if (q.type != null && q.type !== '') {
|
||||
this.warehouseType = q.type
|
||||
}
|
||||
this.reload()
|
||||
},
|
||||
onReachBottom() {
|
||||
this.load(this.page + 1, true)
|
||||
},
|
||||
methods: {
|
||||
rowDisplay(item) {
|
||||
return mapWarehouseRow(item)
|
||||
},
|
||||
reload() {
|
||||
this.page = 1
|
||||
this.load(1, false)
|
||||
},
|
||||
load(page, append) {
|
||||
this.loading = true
|
||||
const params = { page, pageSize: 20 }
|
||||
if (this.keyword) params.name = this.keyword
|
||||
if (this.warehouseType !== '' && this.warehouseType != null) {
|
||||
params.type = this.warehouseType
|
||||
}
|
||||
getWarehouseList(params).then(w => {
|
||||
const items = withWxKey((w.data && w.data.items) || [], 'id', 'wh')
|
||||
this.list = append ? this.list.concat(items) : items
|
||||
this.page = page
|
||||
}).finally(() => { this.loading = false })
|
||||
},
|
||||
goDetail(item) {
|
||||
uni.navigateTo({ url: '/subPackages/sub_clinic_admin/warehouse/detail?id=' + item.id })
|
||||
},
|
||||
openPriceEdit(item) {
|
||||
this.priceEditItem = item
|
||||
this.priceModalVisible = true
|
||||
},
|
||||
closePriceEdit() {
|
||||
this.priceModalVisible = false
|
||||
this.priceEditItem = null
|
||||
},
|
||||
onPriceConfirm(price) {
|
||||
if (!this.priceEditItem || this.priceSubmitting) return
|
||||
this.priceSubmitting = true
|
||||
updateWarehousePrice({ id: this.priceEditItem.id, price }).then(w => {
|
||||
if (w.ok) {
|
||||
uni.showToast({ title: '修改成功' })
|
||||
this.closePriceEdit()
|
||||
this.reload()
|
||||
}
|
||||
}).finally(() => { this.priceSubmitting = false })
|
||||
},
|
||||
toggleStatus(item) {
|
||||
const action = item.status === 2 ? '下架' : '上架'
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定' + action + '「' + ((item.drug && item.drug.drug_name) || '') + '」?',
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
updateWarehouseStatus(item.id).then(w => {
|
||||
if (w.ok) {
|
||||
uni.showToast({ title: '操作成功' })
|
||||
item.status = item.status === 2 ? 1 : 2
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.search {
|
||||
display: flex;
|
||||
background: #fff;
|
||||
padding: 16rpx 24rpx;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 24rpx;
|
||||
align-items: center;
|
||||
}
|
||||
.search input {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
.search text {
|
||||
color: #6acdbb;
|
||||
font-size: 28rpx;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
background: #fff;
|
||||
padding: 24rpx;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 16rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
.thumb {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
border-radius: 12rpx;
|
||||
flex-shrink: 0;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.body {
|
||||
flex: 1;
|
||||
margin-left: 20rpx;
|
||||
min-width: 0;
|
||||
padding-right: 12rpx;
|
||||
}
|
||||
.title {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
.sub {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
.actions {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 12rpx;
|
||||
}
|
||||
.tag {
|
||||
font-size: 22rpx;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
.tag.on {
|
||||
color: #6acdbb;
|
||||
background: rgba(106, 205, 187, 0.12);
|
||||
}
|
||||
.tag.off {
|
||||
color: #999;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.btn-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
align-items: flex-end;
|
||||
}
|
||||
.act-btn {
|
||||
font-size: 24rpx;
|
||||
color: #6acdbb;
|
||||
padding: 4rpx 0;
|
||||
}
|
||||
.act-btn.shelf {
|
||||
color: #666;
|
||||
}
|
||||
.empty {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding: 60rpx;
|
||||
}
|
||||
</style>
|
||||
103
subPackages/sub_clinic_admin/withdrawal/apply.vue
Normal file
103
subPackages/sub_clinic_admin/withdrawal/apply.vue
Normal file
@@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<clinic-page-layout title="申请提现" :show-back="true">
|
||||
<view class="tip">可提现余额:{{ balanceText }}</view>
|
||||
<view class="field">
|
||||
<text class="label">提现金额</text>
|
||||
<input v-model="amount" type="digit" placeholder="请输入金额" />
|
||||
</view>
|
||||
<view class="quick">
|
||||
<text v-for="q in quickAmounts" :key="q.key" class="q-btn" @click="amount = String(q.value)">{{ q.value }}</text>
|
||||
</view>
|
||||
<button class="submit" @click="submit" :loading="loading">提交申请</button>
|
||||
</clinic-page-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ClinicPageLayout from '../components/clinic-page-layout.vue'
|
||||
import { formatMoney } from '../common/format.js'
|
||||
import { getClinicAdminBalance, submitWithdrawal } from '@/api/clinicAdmin.js'
|
||||
|
||||
export default {
|
||||
components: { ClinicPageLayout },
|
||||
data() {
|
||||
return {
|
||||
amount: '',
|
||||
balanceText: '-',
|
||||
loading: false,
|
||||
quickAmounts: [
|
||||
{ key: '1000', value: 1000 },
|
||||
{ key: '3000', value: 3000 },
|
||||
{ key: '5000', value: 5000 },
|
||||
{ key: '10000', value: 10000 },
|
||||
],
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
getClinicAdminBalance().then(w => {
|
||||
if (w.ok && w.data) {
|
||||
const b = w.data.balance
|
||||
this.balanceText = b != null ? formatMoney(b) : '-'
|
||||
}
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
const amt = Number(this.amount)
|
||||
if (!amt || amt <= 0) {
|
||||
uni.showToast({ title: '请输入有效金额', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
submitWithdrawal({ amount: amt }).then(w => {
|
||||
if (w.ok) {
|
||||
uni.showToast({ title: '提交成功' })
|
||||
setTimeout(() => uni.navigateBack(), 800)
|
||||
} else {
|
||||
uni.showToast({ title: (w.res && w.res.message) || '提交失败', icon: 'none' })
|
||||
}
|
||||
}).finally(() => { this.loading = false })
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tip {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
margin-bottom: 32rpx;
|
||||
background: #fff;
|
||||
padding: 24rpx;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
.field {
|
||||
background: #fff;
|
||||
padding: 24rpx;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.label {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.quick {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 48rpx;
|
||||
}
|
||||
.q-btn {
|
||||
padding: 12rpx 24rpx;
|
||||
background: #eef8f6;
|
||||
color: #6acdbb;
|
||||
border-radius: 8rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
.submit {
|
||||
background: #6acdbb;
|
||||
color: #fff;
|
||||
border-radius: 44rpx;
|
||||
}
|
||||
</style>
|
||||
143
subPackages/sub_clinic_admin/withdrawal/card-edit.vue
Normal file
143
subPackages/sub_clinic_admin/withdrawal/card-edit.vue
Normal file
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<clinic-page-layout title="编辑账户" :show-back="true">
|
||||
<view class="form-container">
|
||||
|
||||
<view class="form-panel">
|
||||
<view class="form-row" v-for="f in fields" :key="f.key">
|
||||
<text class="row-label">{{ f.label }}</text>
|
||||
<input
|
||||
class="row-input"
|
||||
v-model="form[f.key]"
|
||||
:placeholder="'请输入' + f.label"
|
||||
placeholder-style="color:#C9CDD4"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="action-footer">
|
||||
<button class="btn-submit" @click="save" :loading="loading">保存修改</button>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</clinic-page-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ClinicPageLayout from '../components/clinic-page-layout.vue'
|
||||
import { getClinicAdminCard, saveClinicAdminCard } from '@/api/clinicAdmin.js'
|
||||
|
||||
export default {
|
||||
components: { ClinicPageLayout },
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
form: {
|
||||
bank_user_name: '',
|
||||
bank_card: '',
|
||||
bank_name: '',
|
||||
bank_no: '',
|
||||
bank_account_type: '1',
|
||||
},
|
||||
fields: [
|
||||
{ key: 'bank_user_name', label: '户名' },
|
||||
{ key: 'bank_card', label: '银行卡号' },
|
||||
{ key: 'bank_name', label: '开户行' },
|
||||
{ key: 'bank_no', label: '行号' },
|
||||
],
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
getClinicAdminCard().then(w => {
|
||||
if (!w.ok || !w.data) return
|
||||
const d = w.data
|
||||
this.form.bank_user_name = d.bank_user_name || ''
|
||||
this.form.bank_card = d.bank_card || ''
|
||||
this.form.bank_name = d.bank_name || ''
|
||||
this.form.bank_no = d.bank_no || ''
|
||||
this.form.bank_account_type = String(d.bank_account_type || 1)
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
save() {
|
||||
this.loading = true
|
||||
saveClinicAdminCard(this.form).then(w => {
|
||||
if (w.ok) {
|
||||
uni.showToast({ title: '保存成功' })
|
||||
setTimeout(() => uni.navigateBack(), 800)
|
||||
} else {
|
||||
uni.showToast({ title: (w.res && w.res.message) || '保存失败', icon: 'none' })
|
||||
}
|
||||
}).finally(() => { this.loading = false })
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$theme-primary: #6acdbb;
|
||||
$color-bg-base: #F5F7F8;
|
||||
$color-text-main: #1D2129;
|
||||
$color-text-muted: #4E5969;
|
||||
|
||||
.form-container {
|
||||
background-color: $color-bg-base;
|
||||
min-height: 100vh;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.form-panel {
|
||||
background: #ffffff;
|
||||
border-radius: 24rpx;
|
||||
padding: 0 32rpx;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 36rpx 0;
|
||||
border-bottom: 1rpx solid #F2F3F5;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.row-label {
|
||||
font-size: 28rpx;
|
||||
color: $color-text-main;
|
||||
font-weight: 500;
|
||||
min-width: 140rpx;
|
||||
}
|
||||
|
||||
.row-input {
|
||||
flex: 1;
|
||||
text-align: right; /* 右对齐显得极其整洁 */
|
||||
font-size: 28rpx;
|
||||
color: $color-text-main;
|
||||
}
|
||||
|
||||
.action-footer {
|
||||
margin-top: 60rpx;
|
||||
}
|
||||
|
||||
.btn-submit {
|
||||
background: $theme-primary;
|
||||
color: #fff;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
height: 96rpx;
|
||||
line-height: 96rpx;
|
||||
border-radius: 48rpx;
|
||||
border: none;
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
415
subPackages/sub_clinic_admin/withdrawal/index.vue
Normal file
415
subPackages/sub_clinic_admin/withdrawal/index.vue
Normal file
@@ -0,0 +1,415 @@
|
||||
<template>
|
||||
<clinic-page-layout title="提现管理" :show-back="true">
|
||||
<view class="withdrawal-container">
|
||||
|
||||
<!-- 高级虚拟银行卡片 -->
|
||||
<view class="premium-bank-card">
|
||||
<view class="card-top">
|
||||
<text class="bank-name">{{ cardTitle || '银行卡' }}</text>
|
||||
<text class="card-type">储蓄卡</text>
|
||||
</view>
|
||||
<!-- 使用完整的 maskCard 方法处理卡号显示 -->
|
||||
<view class="card-number">
|
||||
<text class="card-val">{{ maskCard(card.bank_card) || '**** **** **** ----' }}</text>
|
||||
</view>
|
||||
<view class="card-bottom">
|
||||
<text class="bank-sub">开户行:{{ card.bank_name || '-' }}</text>
|
||||
</view>
|
||||
<!-- 装饰几何图案 -->
|
||||
<view class="card-decorator"></view>
|
||||
</view>
|
||||
|
||||
<!-- 核心统计网格 -->
|
||||
<view class="stats-grid">
|
||||
<view v-for="b in balanceItems" :key="b.key" class="stat-island">
|
||||
<text class="stat-label">{{ b.label }}</text>
|
||||
<text class="stat-val">{{ b.display }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 金刚区操作栏 -->
|
||||
<view class="action-bar">
|
||||
<view v-for="m in actions" :key="m.key" class="action-item" hover-class="action-hover" @click="onAction(m.key)">
|
||||
<text class="action-text">{{ m.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 提现记录列表 -->
|
||||
<view class="list-section">
|
||||
<view class="section-hd">
|
||||
<text class="section-title">提现记录</text>
|
||||
</view>
|
||||
|
||||
<view class="record-list">
|
||||
<view
|
||||
v-for="item in withdrawalList"
|
||||
:key="item._wxKey"
|
||||
class="record-item"
|
||||
hover-class="item-hover"
|
||||
@click="goWithdrawalDetail(item)"
|
||||
>
|
||||
<view class="record-main">
|
||||
<text class="record-title">提现到银行卡</text>
|
||||
<!-- 三元表达式处理颜色,完美兼容微信小程序 -->
|
||||
<text class="record-amount" :class="item.status === 1 ? 'amount-success' : ''">-{{ item.amount || '0.00' }}</text>
|
||||
</view>
|
||||
<view class="record-sub">
|
||||
<text class="record-time">{{ item.time || '-' }}</text>
|
||||
<text
|
||||
class="record-status"
|
||||
:class="item.status === 1 ? 'text-success' : (item.status === 2 || item.status === 3 ? 'text-danger' : 'text-warning')"
|
||||
>
|
||||
{{ item.statusText || '处理中' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="withdrawalList.length === 0" class="empty-state">
|
||||
暂无提现记录
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</clinic-page-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ClinicPageLayout from '../components/clinic-page-layout.vue'
|
||||
import { getClinicAdminCard, getClinicAdminBalance, getWithdrawalList } from '@/api/clinicAdmin.js'
|
||||
import { withWxKey } from '@/utils/wxListKey.js'
|
||||
import { formatMoney } from '../common/format.js'
|
||||
import { withdrawalCheckStatusText, formatDateTime } from '../common/display.js'
|
||||
|
||||
export default {
|
||||
components: { ClinicPageLayout },
|
||||
data() {
|
||||
return {
|
||||
cardTitle: '我的账户',
|
||||
card: {
|
||||
bank_card: '',
|
||||
bank_name: ''
|
||||
},
|
||||
balanceItems: [
|
||||
{ key: 'balance', label: '可提现余额 (元)', display: '0.00' },
|
||||
{ key: 'frozen', label: '冻结金额 (元)', display: '0.00' }
|
||||
],
|
||||
actions: [
|
||||
{ key: 'apply', label: '申请提现' },
|
||||
{ key: 'edit', label: '修改账户' },
|
||||
{ key: 'settlement', label: '结算记录' }
|
||||
],
|
||||
withdrawalList: [],
|
||||
page: 1,
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
this.loadCard();
|
||||
this.loadBalance();
|
||||
this.reloadList();
|
||||
},
|
||||
onReachBottom() {
|
||||
if (!this.loading) {
|
||||
this.loadList(this.page + 1, true);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 银行卡号掩码处理,保留后四位
|
||||
maskCard(cardStr) {
|
||||
if(!cardStr) return '**** **** **** ----';
|
||||
const last4 = cardStr.slice(-4);
|
||||
return `**** **** **** ${last4}`;
|
||||
},
|
||||
|
||||
// 快捷操作跳转
|
||||
onAction(key) {
|
||||
if (key === 'apply') {
|
||||
uni.navigateTo({ url: '/subPackages/sub_clinic_admin/withdrawal/apply' });
|
||||
} else if (key === 'edit') {
|
||||
uni.navigateTo({ url: '/subPackages/sub_clinic_admin/withdrawal/card-edit' });
|
||||
} else if (key === 'settlement') {
|
||||
uni.navigateTo({ url: '/subPackages/sub_clinic_admin/withdrawal/settlement/index' });
|
||||
}
|
||||
},
|
||||
|
||||
// 提现详情跳转
|
||||
goWithdrawalDetail(item) {
|
||||
uni.navigateTo({ url: `/subPackages/sub_clinic_admin/withdrawal/record-detail?id=${item.id}` });
|
||||
},
|
||||
|
||||
// 获取绑定的银行卡信息
|
||||
loadCard() {
|
||||
getClinicAdminCard().then(w => {
|
||||
if (w.ok && w.data) {
|
||||
this.card = w.data;
|
||||
this.cardTitle = w.data.bank_user_name || '我的账户';
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 获取余额数据
|
||||
loadBalance() {
|
||||
getClinicAdminBalance().then(w => {
|
||||
if (w.ok && w.data) {
|
||||
// 格式化金额并去除 ¥ 符号以便大字号展示
|
||||
this.balanceItems[0].display = formatMoney(w.data.balance || 0).replace('¥', '');
|
||||
this.balanceItems[1].display = formatMoney(w.data.frozen_balance || 0).replace('¥', '');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 刷新提现列表
|
||||
reloadList() {
|
||||
this.page = 1;
|
||||
this.loadList(1, false);
|
||||
},
|
||||
|
||||
// 加载提现列表数据
|
||||
loadList(page, append) {
|
||||
this.loading = true;
|
||||
getWithdrawalList({ page, pageSize: 15 }).then(w => {
|
||||
if (w.ok && w.data) {
|
||||
const items = (w.data.items || []).map(item => ({
|
||||
...item,
|
||||
amount: formatMoney(item.apply_cash).replace('¥', ''),
|
||||
time: formatDateTime(item.apply_time),
|
||||
status: item.check_status,
|
||||
statusText: withdrawalCheckStatusText(item.check_status)
|
||||
}));
|
||||
|
||||
const listWithKey = withWxKey(items, 'id', 'wd');
|
||||
this.withdrawalList = append ? this.withdrawalList.concat(listWithKey) : listWithKey;
|
||||
this.page = page;
|
||||
}
|
||||
}).finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$theme-primary: #6acdbb;
|
||||
$color-bg-base: #F5F7F8;
|
||||
$color-text-main: #1D2129;
|
||||
$color-text-body: #4E5969;
|
||||
$color-text-muted: #86909C;
|
||||
|
||||
.withdrawal-container {
|
||||
background-color: $color-bg-base;
|
||||
min-height: 100vh;
|
||||
padding: 24rpx 24rpx 60rpx;
|
||||
}
|
||||
|
||||
/* 高级虚拟银行卡 */
|
||||
.premium-bank-card {
|
||||
position: relative;
|
||||
background: linear-gradient(135deg, #2C303A 0%, #1A1D24 100%);
|
||||
border-radius: 32rpx;
|
||||
padding: 40rpx;
|
||||
color: #fff;
|
||||
box-shadow: 0 16rpx 32rpx rgba(26, 29, 36, 0.15);
|
||||
margin-bottom: 24rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 40rpx;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.bank-name {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
.card-type {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255,255,255,0.6);
|
||||
}
|
||||
|
||||
.card-number {
|
||||
font-family: monospace, sans-serif;
|
||||
font-size: 44rpx;
|
||||
font-weight: 600;
|
||||
margin-bottom: 40rpx;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
.card-bottom {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255,255,255,0.6);
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.card-decorator {
|
||||
position: absolute;
|
||||
right: -40rpx;
|
||||
bottom: -60rpx;
|
||||
width: 240rpx;
|
||||
height: 240rpx;
|
||||
border: 40rpx solid rgba(255,255,255,0.03);
|
||||
border-radius: 50%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 统计网格 */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 20rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.stat-island {
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
padding: 32rpx 24rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 24rpx;
|
||||
color: $color-text-muted;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.stat-val {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: $color-text-main;
|
||||
font-family: DIN Alternate, monospace, sans-serif;
|
||||
}
|
||||
|
||||
/* 金刚区操作栏 */
|
||||
.action-bar {
|
||||
display: flex;
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
margin-bottom: 32rpx;
|
||||
padding: 12rpx 0;
|
||||
}
|
||||
|
||||
.action-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24rpx 0;
|
||||
position: relative;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:not(:last-child)::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 30%;
|
||||
height: 40%;
|
||||
width: 2rpx;
|
||||
background-color: #F2F3F5;
|
||||
}
|
||||
}
|
||||
|
||||
.action-hover {
|
||||
background-color: #FAFAFA;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
color: $color-text-main;
|
||||
}
|
||||
|
||||
/* 提现记录列表 */
|
||||
.list-section {
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
padding: 32rpx 24rpx;
|
||||
}
|
||||
|
||||
.section-hd {
|
||||
margin-bottom: 24rpx;
|
||||
padding-left: 8rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: $color-text-main;
|
||||
}
|
||||
|
||||
.record-item {
|
||||
padding: 24rpx 8rpx;
|
||||
border-bottom: 1rpx solid #F2F3F5;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.item-hover {
|
||||
background-color: #F7F8FA;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.record-main {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.record-title {
|
||||
font-size: 28rpx;
|
||||
color: $color-text-main;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.record-amount {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: $color-text-main;
|
||||
font-family: DIN Alternate, monospace, sans-serif;
|
||||
|
||||
&.amount-success {
|
||||
color: $theme-primary; /* 成功的金额变绿 */
|
||||
}
|
||||
}
|
||||
|
||||
.record-sub {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.record-time {
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
.record-status {
|
||||
font-weight: 500;
|
||||
}
|
||||
.text-success { color: $theme-primary; }
|
||||
.text-danger { color: #F53F3F; }
|
||||
.text-warning { color: #F5A623; }
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
color: $color-text-muted;
|
||||
padding: 60rpx 0;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
</style>
|
||||
170
subPackages/sub_clinic_admin/withdrawal/record-detail.vue
Normal file
170
subPackages/sub_clinic_admin/withdrawal/record-detail.vue
Normal file
@@ -0,0 +1,170 @@
|
||||
<template>
|
||||
<clinic-page-layout title="提现详情" :show-back="true">
|
||||
<view class="detail-container">
|
||||
|
||||
<view v-if="info.id" class="detail-card">
|
||||
<!-- 顶部焦点区域 -->
|
||||
<view class="hero-section">
|
||||
<text class="hero-label">实际到账 (元)</text>
|
||||
<text class="hero-amount">{{ formatMoney(info.true_cash) }}</text>
|
||||
|
||||
<!-- 修复:使用三元表达式代替 methods 函数调用 -->
|
||||
<view
|
||||
class="status-badge"
|
||||
:class="info.check_status === 1 ? 'badge-success' : (info.check_status === 3 ? 'badge-danger' : 'badge-warning')"
|
||||
>
|
||||
{{ checkStatusText(info.check_status) }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 明细信息 -->
|
||||
<view class="info-list">
|
||||
<view class="row"><text class="label">订单号</text><text class="value font-mono">{{ info.order_no || '-' }}</text></view>
|
||||
<view class="row"><text class="label">申请金额</text><text class="value">{{ formatMoney(info.apply_cash) }}</text></view>
|
||||
<view class="row"><text class="label">手续费</text><text class="value">{{ formatMoney(info.charge_cash) }}</text></view>
|
||||
<view class="row"><text class="label">申请时间</text><text class="value">{{ formatDateTime(info.apply_time) }}</text></view>
|
||||
|
||||
<!-- 审核说明(突出显示拒绝原因) -->
|
||||
<view class="row" v-if="info.check_result">
|
||||
<text class="label">审核说明</text>
|
||||
<text class="value" :class="{'text-danger': info.check_status === 3}">{{ info.check_result }}</text>
|
||||
</view>
|
||||
|
||||
<view class="divider"></view>
|
||||
|
||||
<view class="row"><text class="label">打款状态</text><text class="value">{{ dakuanStatusText(info.dakuan_status) }}</text></view>
|
||||
<view class="row" v-if="info.dakuan_time"><text class="label">打款时间</text><text class="value">{{ formatDateTime(info.dakuan_time) }}</text></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</clinic-page-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ClinicPageLayout from '../components/clinic-page-layout.vue'
|
||||
import { formatMoney } from '../common/format.js'
|
||||
import {
|
||||
formatDateTime,
|
||||
withdrawalCheckStatusText,
|
||||
dakuanStatusText as dakuanStatusLabel,
|
||||
} from '../common/display.js'
|
||||
import { getWithdrawalDetail } from '@/api/clinicAdmin.js'
|
||||
|
||||
export default {
|
||||
components: { ClinicPageLayout },
|
||||
data() {
|
||||
return { id: 0, info: {} }
|
||||
},
|
||||
onLoad(q) {
|
||||
this.id = Number(q.id || 0)
|
||||
getWithdrawalDetail(this.id).then(w => {
|
||||
if (w.ok) this.info = w.data || {}
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
formatMoney,
|
||||
formatDateTime,
|
||||
checkStatusText(s) {
|
||||
return withdrawalCheckStatusText(s)
|
||||
},
|
||||
dakuanStatusText(s) {
|
||||
return dakuanStatusLabel(s)
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$theme-primary: #6acdbb;
|
||||
$color-bg-base: #F5F7F8;
|
||||
$color-text-main: #1D2129;
|
||||
$color-text-muted: #86909C;
|
||||
|
||||
.detail-container {
|
||||
background-color: $color-bg-base;
|
||||
min-height: 100vh;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
background: #ffffff;
|
||||
border-radius: 24rpx;
|
||||
padding: 48rpx 32rpx;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.hero-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 60rpx;
|
||||
}
|
||||
|
||||
.hero-label {
|
||||
font-size: 26rpx;
|
||||
color: $color-text-muted;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.hero-amount {
|
||||
font-size: 64rpx;
|
||||
font-weight: 700;
|
||||
color: $color-text-main;
|
||||
font-family: DIN Alternate, monospace, sans-serif;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
font-size: 24rpx;
|
||||
padding: 6rpx 24rpx;
|
||||
border-radius: 30rpx;
|
||||
font-weight: 500;
|
||||
|
||||
&.badge-success { background: rgba(106, 205, 187, 0.1); color: $theme-primary; }
|
||||
&.badge-warning { background: #FFF3E8; color: #FF7D00; }
|
||||
&.badge-danger { background: #FFECE8; color: #F53F3F; }
|
||||
}
|
||||
|
||||
.info-list {
|
||||
background: #F8FBFB;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx 32rpx;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
padding: 16rpx 0;
|
||||
font-size: 26rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: $color-text-muted;
|
||||
min-width: 120rpx;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: $color-text-main;
|
||||
text-align: right;
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.font-mono {
|
||||
font-family: SF Pro Text, monospace, sans-serif;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: #F53F3F;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1rpx;
|
||||
background-color: #EAEFEF;
|
||||
margin: 16rpx 0;
|
||||
}
|
||||
</style>
|
||||
196
subPackages/sub_clinic_admin/withdrawal/settlement/detail.vue
Normal file
196
subPackages/sub_clinic_admin/withdrawal/settlement/detail.vue
Normal file
@@ -0,0 +1,196 @@
|
||||
<template>
|
||||
<clinic-page-layout title="结算明细" :show-back="true">
|
||||
<view class="detail-container">
|
||||
|
||||
<!-- 顶部焦点区域: 订单号和合计金额 -->
|
||||
<view class="hero-card" v-if="detail.order_no">
|
||||
<text class="hero-label">订单号:{{ detail.order_no }}</text>
|
||||
<text class="hero-amount">{{ formatMoney(detail.sum_money) }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 汇总信息岛 -->
|
||||
<view class="info-island" v-if="ledgerLogSummary">
|
||||
<view class="island-title">结算汇总</view>
|
||||
<view class="row"><text class="label">费用类型</text><text class="value">{{ ledgerLogSummary.fee_type_txt || '-' }}</text></view>
|
||||
<view class="row"><text class="label">结算类型</text><text class="value">{{ ledgerLogSummary.type_txt || '-' }}</text></view>
|
||||
<view class="row"><text class="label">结算总额</text><text class="value text-primary font-bold">{{ formatMoney(ledgerLogSummary.amount) }}</text></view>
|
||||
<view class="row" v-if="ledgerLogSummary.content"><text class="label">说明</text><text class="value">{{ ledgerLogSummary.content }}</text></view>
|
||||
</view>
|
||||
|
||||
<!-- 分账明细列表 -->
|
||||
<view v-if="ledgerItems.length">
|
||||
<view class="section-title">分账明细</view>
|
||||
<view class="list-wrap">
|
||||
<view v-for="row in ledgerItems" :key="row._wxKey" class="detail-card">
|
||||
<view class="row"><text class="label">费用类型</text><text class="value">{{ row.fee_type_txt || '-' }}</text></view>
|
||||
<view class="row"><text class="label">分账金额</text><text class="value amount-text">{{ formatMoney(row.money) }}</text></view>
|
||||
<view class="row">
|
||||
<text class="label">结算状态</text>
|
||||
<text class="value" :class="row.status_txt === '已结算' ? 'text-success' : ''">{{ row.status_txt || '-' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</clinic-page-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ClinicPageLayout from '../../components/clinic-page-layout.vue'
|
||||
import { formatMoney } from '../../common/format.js'
|
||||
import { getLedgerDetail } from '@/api/clinicAdmin.js'
|
||||
import { withWxKey } from '@/utils/wxListKey.js'
|
||||
|
||||
export default {
|
||||
components: { ClinicPageLayout },
|
||||
data() {
|
||||
return {
|
||||
ledgerLogId: 0,
|
||||
detail: { items: [], sum_money: '0' },
|
||||
ledgerLogSummary: null,
|
||||
ledgerItems: [],
|
||||
loading: true,
|
||||
}
|
||||
},
|
||||
onLoad(q) {
|
||||
this.ledgerLogId = Number(q.ledger_log_id || 0)
|
||||
this.load()
|
||||
},
|
||||
methods: {
|
||||
formatMoney,
|
||||
load() {
|
||||
this.loading = true
|
||||
getLedgerDetail({ ledger_log_id: this.ledgerLogId }).then(w => {
|
||||
if (w.ok) {
|
||||
this.detail = w.data || {}
|
||||
const log = this.detail.ledger_log
|
||||
if (log) {
|
||||
this.ledgerLogSummary = log
|
||||
}
|
||||
this.ledgerItems = withWxKey(this.detail.items || [], 'id', 'led')
|
||||
}
|
||||
}).finally(() => { this.loading = false })
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$theme-primary: #6acdbb;
|
||||
$color-bg-base: #F5F7F8;
|
||||
$color-text-main: #1D2129;
|
||||
$color-text-muted: #86909C;
|
||||
|
||||
.detail-container {
|
||||
background-color: $color-bg-base;
|
||||
min-height: 100vh;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
/* 巨幕卡片 */
|
||||
.hero-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background: #ffffff;
|
||||
border-radius: 24rpx;
|
||||
padding: 48rpx 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.hero-label {
|
||||
font-size: 26rpx;
|
||||
color: $color-text-muted;
|
||||
margin-bottom: 12rpx;
|
||||
font-family: SF Pro Text, monospace, sans-serif;
|
||||
}
|
||||
|
||||
.hero-amount {
|
||||
font-size: 64rpx;
|
||||
font-weight: 700;
|
||||
color: $color-text-main;
|
||||
font-family: DIN Alternate, monospace, sans-serif;
|
||||
}
|
||||
|
||||
/* 汇总信息岛 */
|
||||
.info-island {
|
||||
background: #ffffff;
|
||||
border-radius: 24rpx;
|
||||
padding: 32rpx;
|
||||
margin-bottom: 32rpx;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.island-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: $color-text-main;
|
||||
margin-bottom: 24rpx;
|
||||
padding-bottom: 16rpx;
|
||||
border-bottom: 1rpx solid #F2F3F5;
|
||||
}
|
||||
|
||||
/* 分账明细 */
|
||||
.section-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: $color-text-muted;
|
||||
margin-bottom: 16rpx;
|
||||
padding-left: 8rpx;
|
||||
}
|
||||
|
||||
.list-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
background: #F8FBFB;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx 32rpx;
|
||||
}
|
||||
|
||||
/* 通用行排版 */
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
padding: 12rpx 0;
|
||||
font-size: 26rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: $color-text-muted;
|
||||
min-width: 120rpx;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: $color-text-main;
|
||||
text-align: right;
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
|
||||
&.text-primary {
|
||||
color: $theme-primary;
|
||||
}
|
||||
&.font-bold {
|
||||
font-weight: 700;
|
||||
font-size: 32rpx;
|
||||
font-family: DIN Alternate, monospace, sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
.amount-text {
|
||||
font-weight: 600;
|
||||
font-family: DIN Alternate, monospace, sans-serif;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: $theme-primary;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
194
subPackages/sub_clinic_admin/withdrawal/settlement/index.vue
Normal file
194
subPackages/sub_clinic_admin/withdrawal/settlement/index.vue
Normal file
@@ -0,0 +1,194 @@
|
||||
<template>
|
||||
<clinic-page-layout title="结算记录" :show-back="true">
|
||||
<view class="settlement-container">
|
||||
|
||||
<view class="list-wrap">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="premium-card"
|
||||
hover-class="card-hover"
|
||||
@click="goDetail(item)"
|
||||
>
|
||||
<view class="card-head">
|
||||
<view class="order-no">
|
||||
<text class="tag">单</text>
|
||||
<text class="no-text">{{ rowDisplay(item).orderNo }}</text>
|
||||
</view>
|
||||
<text class="amount" :class="rowDisplay(item).amountClass">
|
||||
{{ rowDisplay(item).typeText }} {{ rowDisplay(item).amountText }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="card-body">
|
||||
<view class="info-row">
|
||||
<text class="label">费用类型</text>
|
||||
<text class="value">{{ rowDisplay(item).feeTypeText }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="label">结算时间</text>
|
||||
<text class="value">{{ rowDisplay(item).createdAt }}</text>
|
||||
</view>
|
||||
<view class="info-row" v-if="rowDisplay(item).content">
|
||||
<text class="label">费用说明</text>
|
||||
<text class="value">{{ rowDisplay(item).content }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="!loading && list.length === 0" class="empty-state">
|
||||
暂无结算记录
|
||||
</view>
|
||||
</view>
|
||||
</clinic-page-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ClinicPageLayout from '../../components/clinic-page-layout.vue'
|
||||
import { mapSettlementRow } from '../../common/display.js'
|
||||
import { getLedgerLogList } from '@/api/clinicAdmin.js'
|
||||
|
||||
export default {
|
||||
components: { ClinicPageLayout },
|
||||
data() {
|
||||
return {
|
||||
list: [],
|
||||
page: 1,
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
this.reload()
|
||||
},
|
||||
onReachBottom() {
|
||||
this.load(this.page + 1, true)
|
||||
},
|
||||
methods: {
|
||||
rowDisplay(item) {
|
||||
return mapSettlementRow(item)
|
||||
},
|
||||
reload() {
|
||||
this.load(1, false)
|
||||
},
|
||||
load(page, append) {
|
||||
this.loading = true
|
||||
getLedgerLogList({ page, pageSize: 15 }).then(w => {
|
||||
const items = (w.data && w.data.items) || []
|
||||
this.list = append ? this.list.concat(items) : items
|
||||
this.page = page
|
||||
}).finally(() => { this.loading = false })
|
||||
},
|
||||
goDetail(item) {
|
||||
uni.navigateTo({
|
||||
url: '/subPackages/sub_clinic_admin/withdrawal/settlement/detail?ledger_log_id=' + item.id,
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$color-bg-base: #F5F7F8;
|
||||
$color-text-main: #1D2129;
|
||||
$color-text-body: #4E5969;
|
||||
$color-text-muted: #86909C;
|
||||
|
||||
.settlement-container {
|
||||
background-color: $color-bg-base;
|
||||
min-height: 100vh;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.list-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.premium-card {
|
||||
background: #ffffff;
|
||||
border-radius: 24rpx;
|
||||
padding: 32rpx;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.02);
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.card-hover {
|
||||
background-color: #FAFAFA;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.card-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24rpx;
|
||||
border-bottom: 1rpx dashed #E5E6EB;
|
||||
padding-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.order-no {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.tag {
|
||||
font-size: 20rpx;
|
||||
color: #fff;
|
||||
background-color: #C9CDD4;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 6rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.no-text {
|
||||
font-size: 26rpx;
|
||||
color: $color-text-main;
|
||||
font-family: monospace, sans-serif;
|
||||
}
|
||||
|
||||
.amount {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
font-family: DIN Alternate, monospace, sans-serif;
|
||||
}
|
||||
|
||||
/* 兼容你原有的类名逻辑 */
|
||||
.text-danger { color: #F53F3F !important; }
|
||||
.text-success { color: #6acdbb !important; }
|
||||
.text-warning { color: #F5A623 !important; }
|
||||
|
||||
.card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 26rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: $color-text-muted;
|
||||
min-width: 120rpx;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: $color-text-body;
|
||||
text-align: right;
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
color: $color-text-muted;
|
||||
padding: 120rpx 0;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
</style>
|
||||
23
utils/wxListKey.js
Normal file
23
utils/wxListKey.js
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* 微信小程序 v-for :key 仅支持简单属性访问,不支持模板表达式。
|
||||
* 在 JS 中为列表项预生成 _wxKey 后,模板使用 :key="item._wxKey"
|
||||
*/
|
||||
export function withWxKey(list, idField = 'id', fallbackPrefix = 'i') {
|
||||
return (list || []).map((item, index) => ({
|
||||
...item,
|
||||
_wxKey: item[idField] != null && item[idField] !== ''
|
||||
? String(item[idField])
|
||||
: `${fallbackPrefix}${index}`,
|
||||
}))
|
||||
}
|
||||
|
||||
/** 单条消息/列表项补全 _wxKey(用于 push、$set 等增量更新) */
|
||||
export function ensureWxKey(item, idField = 'id', fallback) {
|
||||
if (!item || typeof item !== 'object') return item
|
||||
if (item._wxKey != null && item._wxKey !== '') return item
|
||||
const id = item[idField]
|
||||
return {
|
||||
...item,
|
||||
_wxKey: id != null && id !== '' ? String(id) : String(fallback),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user