1. 优化门店管理员的功能
This commit is contained in:
191
api/businessApi.js
Normal file
191
api/businessApi.js
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
import { req } from '@/common/js/index.js';
|
||||||
|
import { unwrapClinicRes } from '@/api/clinicAdmin.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按前缀生成业务 API(订单/对账/提现/仓库等)
|
||||||
|
*/
|
||||||
|
export function createBusinessApi(prefix, headerFlag) {
|
||||||
|
const headerKey = headerFlag || '_businessAdmin';
|
||||||
|
function bizRequest(options) {
|
||||||
|
return req.request({
|
||||||
|
...options,
|
||||||
|
header: {
|
||||||
|
...(options.header || {}),
|
||||||
|
[headerKey]: '1',
|
||||||
|
},
|
||||||
|
}).then((res) => {
|
||||||
|
const data = unwrapClinicRes(res);
|
||||||
|
const ok = res && (res.code === 0 || (res.errcode != null && Number(res.errcode) !== -1));
|
||||||
|
return { res, data, ok };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
prefix,
|
||||||
|
getMyInfo() {
|
||||||
|
return bizRequest({ url: `${prefix}-auth/my-info`, method: 'GET' });
|
||||||
|
},
|
||||||
|
getInputStats() {
|
||||||
|
return bizRequest({ url: `${prefix}-auth/input-stats`, method: 'GET' });
|
||||||
|
},
|
||||||
|
getMyBalance() {
|
||||||
|
return bizRequest({ url: `${prefix}-account/my-balance`, method: 'GET' });
|
||||||
|
},
|
||||||
|
getMyCard() {
|
||||||
|
return bizRequest({ url: `${prefix}-account/my-card`, method: 'GET' });
|
||||||
|
},
|
||||||
|
saveMyCard(data) {
|
||||||
|
return bizRequest({ url: `${prefix}-account/save-card`, method: 'POST', data });
|
||||||
|
},
|
||||||
|
getLedgerLogList(params) {
|
||||||
|
return bizRequest({ url: `${prefix}-settlement/ledger-log-list`, method: 'GET', data: params });
|
||||||
|
},
|
||||||
|
getLedgerDetail(params) {
|
||||||
|
return bizRequest({ url: `${prefix}-settlement/ledger-detail`, method: 'GET', data: params });
|
||||||
|
},
|
||||||
|
getAccountChangeLogList(params) {
|
||||||
|
return bizRequest({ url: `${prefix}-account-change-log/list`, method: 'GET', data: params });
|
||||||
|
},
|
||||||
|
getWithdrawalList(params) {
|
||||||
|
return bizRequest({ url: `${prefix}-withdrawal/list`, method: 'GET', data: params });
|
||||||
|
},
|
||||||
|
getWithdrawalAuditList(params) {
|
||||||
|
return bizRequest({ url: `${prefix}-withdrawal/audit-list`, method: 'GET', data: params });
|
||||||
|
},
|
||||||
|
getWithdrawalDetail(id) {
|
||||||
|
return bizRequest({ url: `${prefix}-withdrawal/detail`, method: 'GET', data: { id } });
|
||||||
|
},
|
||||||
|
submitWithdrawal(data) {
|
||||||
|
return bizRequest({ url: `${prefix}-withdrawal/withdrawal`, method: 'POST', data });
|
||||||
|
},
|
||||||
|
passWithdrawal(data) {
|
||||||
|
return bizRequest({ url: `${prefix}-withdrawal/pass-application`, method: 'POST', data });
|
||||||
|
},
|
||||||
|
refuseWithdrawal(data) {
|
||||||
|
return bizRequest({ url: `${prefix}-withdrawal/refuse-application`, method: 'POST', data });
|
||||||
|
},
|
||||||
|
getReconciliationList(params) {
|
||||||
|
return bizRequest({ url: `${prefix}-reconciliation/list`, method: 'GET', data: params });
|
||||||
|
},
|
||||||
|
getReconciliationDrugList(params) {
|
||||||
|
return bizRequest({ url: `${prefix}-reconciliation/drug-list`, method: 'GET', data: params });
|
||||||
|
},
|
||||||
|
getReconciliationDrugOptions(params) {
|
||||||
|
return bizRequest({ url: `${prefix}-reconciliation/drug-options`, method: 'GET', data: params });
|
||||||
|
},
|
||||||
|
getReconciliationOrderOptions(params) {
|
||||||
|
return bizRequest({ url: `${prefix}-reconciliation/order-options`, method: 'GET', data: params });
|
||||||
|
},
|
||||||
|
getOrderSaleAmount(params) {
|
||||||
|
return bizRequest({ url: `${prefix}-order/sale-amount`, method: 'GET', data: params });
|
||||||
|
},
|
||||||
|
getPrescriptionDetail(id) {
|
||||||
|
return bizRequest({ url: `${prefix}-prescription/detail`, method: 'GET', data: { id } });
|
||||||
|
},
|
||||||
|
getOrderList(params) {
|
||||||
|
return bizRequest({ url: `${prefix}-order/list`, method: 'GET', data: params });
|
||||||
|
},
|
||||||
|
getOrderStatusOption() {
|
||||||
|
return bizRequest({ url: `${prefix}-order/status-option`, method: 'GET' });
|
||||||
|
},
|
||||||
|
getOrderDetail(id) {
|
||||||
|
return bizRequest({ url: `${prefix}-order/detail`, method: 'GET', data: { id } });
|
||||||
|
},
|
||||||
|
getOrderTrace(params) {
|
||||||
|
return bizRequest({ url: `${prefix}-order/trace`, method: 'GET', data: params });
|
||||||
|
},
|
||||||
|
getOrderLedgerDetail(params) {
|
||||||
|
return bizRequest({ url: `${prefix}-order/ledger-detail`, method: 'GET', data: params });
|
||||||
|
},
|
||||||
|
getOrderReconciliationDetail(params) {
|
||||||
|
return bizRequest({ url: `${prefix}-order/reconciliation-detail`, method: 'GET', data: params });
|
||||||
|
},
|
||||||
|
refundOrder(data) {
|
||||||
|
return bizRequest({ url: `${prefix}-order/refund`, method: 'POST', data });
|
||||||
|
},
|
||||||
|
wareSendOrder(data) {
|
||||||
|
return bizRequest({ url: `${prefix}-order/ware-send`, method: 'POST', data });
|
||||||
|
},
|
||||||
|
getExpressDetailByOrderId(orderId) {
|
||||||
|
return bizRequest({
|
||||||
|
url: `${prefix}-express-detail/detail-by-order`,
|
||||||
|
method: 'POST',
|
||||||
|
data: { order_id: orderId },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getWarehouseList(params) {
|
||||||
|
return bizRequest({ url: `${prefix}-warehouse/list`, method: 'GET', data: params });
|
||||||
|
},
|
||||||
|
getWarehouseDetail(id) {
|
||||||
|
return bizRequest({ url: `${prefix}-warehouse/detail`, method: 'GET', data: { id } });
|
||||||
|
},
|
||||||
|
updateWarehouseStatus(id) {
|
||||||
|
return bizRequest({ url: `${prefix}-warehouse/update-status`, method: 'GET', data: { id } });
|
||||||
|
},
|
||||||
|
updateWarehousePrice(data) {
|
||||||
|
return bizRequest({ url: `${prefix}-warehouse/update`, method: 'POST', data });
|
||||||
|
},
|
||||||
|
getWarehouseProductTypeOptions() {
|
||||||
|
return bizRequest({ url: `${prefix}-warehouse/product-type-options`, method: 'GET' });
|
||||||
|
},
|
||||||
|
getStoreOption() {
|
||||||
|
return bizRequest({ url: `${prefix}-store/option`, method: 'GET' });
|
||||||
|
},
|
||||||
|
getStoreInputList(params) {
|
||||||
|
return bizRequest({ url: `${prefix}-store-input/list`, method: 'GET', data: params });
|
||||||
|
},
|
||||||
|
getStoreInputDetail(id) {
|
||||||
|
return bizRequest({ url: `${prefix}-store-input/detail`, method: 'GET', data: { id } });
|
||||||
|
},
|
||||||
|
getPromoterOptions() {
|
||||||
|
return bizRequest({ url: `${prefix}-store-input/promoter-options`, method: 'GET' });
|
||||||
|
},
|
||||||
|
getStoreInputOption() {
|
||||||
|
return bizRequest({ url: `${prefix}-store-input/option`, method: 'GET' });
|
||||||
|
},
|
||||||
|
createStoreInput(data) {
|
||||||
|
return bizRequest({ url: `${prefix}-store-input/create`, method: 'POST', data });
|
||||||
|
},
|
||||||
|
updateStoreInput(data) {
|
||||||
|
return bizRequest({ url: `${prefix}-store-input/update`, method: 'POST', data });
|
||||||
|
},
|
||||||
|
auditStoreInput(data) {
|
||||||
|
return bizRequest({ url: `${prefix}-store-input/audit`, method: 'POST', data });
|
||||||
|
},
|
||||||
|
getDoctorInputList(params) {
|
||||||
|
return bizRequest({ url: `${prefix}-doctor-input/list`, method: 'GET', data: params });
|
||||||
|
},
|
||||||
|
getDoctorInputDetail(id) {
|
||||||
|
return bizRequest({ url: `${prefix}-doctor-input/detail`, method: 'GET', data: { id } });
|
||||||
|
},
|
||||||
|
createDoctorInput(data) {
|
||||||
|
return bizRequest({ url: `${prefix}-doctor-input/create`, method: 'POST', data });
|
||||||
|
},
|
||||||
|
updateDoctorInput(data) {
|
||||||
|
return bizRequest({ url: `${prefix}-doctor-input/update`, method: 'POST', data });
|
||||||
|
},
|
||||||
|
auditDoctorInput(data) {
|
||||||
|
return bizRequest({ url: `${prefix}-doctor-input/audit`, method: 'POST', data });
|
||||||
|
},
|
||||||
|
getDoctorTitleOption() {
|
||||||
|
return bizRequest({
|
||||||
|
url: `${prefix}-pharmacist/title-option`,
|
||||||
|
method: 'GET',
|
||||||
|
data: { type: 1 },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getDepartOption() {
|
||||||
|
return bizRequest({ url: `${prefix}-doctor/depart-option`, method: 'GET' });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const platformAdminApi = createBusinessApi('/newApi/platform-admin', '_platformAdmin');
|
||||||
|
export const salespersonApi = createBusinessApi('/newApi/salesperson', '_salesperson');
|
||||||
|
|
||||||
|
export function getActiveBusinessApi() {
|
||||||
|
const mode = uni.getStorageSync('loginMode');
|
||||||
|
if (mode === 'platform_admin') return platformAdminApi;
|
||||||
|
if (mode === 'salesperson') return salespersonApi;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -47,6 +47,10 @@ export function getClinicAdminMyInfo() {
|
|||||||
return clinicRequest({ url: `${PREFIX}-auth/my-info`, method: 'GET' });
|
return clinicRequest({ url: `${PREFIX}-auth/my-info`, method: 'GET' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getClinicAdminStoreQr() {
|
||||||
|
return clinicRequest({ url: `${PREFIX}-auth/store-qr`, method: 'GET' });
|
||||||
|
}
|
||||||
|
|
||||||
export function getClinicAdminBalance() {
|
export function getClinicAdminBalance() {
|
||||||
return clinicRequest({ url: `${PREFIX}-account/my-balance`, method: 'GET' });
|
return clinicRequest({ url: `${PREFIX}-account/my-balance`, method: 'GET' });
|
||||||
}
|
}
|
||||||
@@ -59,6 +63,10 @@ export function saveClinicAdminCard(data) {
|
|||||||
return clinicRequest({ url: `${PREFIX}-account/save-card`, method: 'POST', data });
|
return clinicRequest({ url: `${PREFIX}-account/save-card`, method: 'POST', data });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getMyBalance = getClinicAdminBalance;
|
||||||
|
export const getMyCard = getClinicAdminCard;
|
||||||
|
export const saveMyCard = saveClinicAdminCard;
|
||||||
|
|
||||||
export function getLedgerLogList(params) {
|
export function getLedgerLogList(params) {
|
||||||
return clinicRequest({ url: `${PREFIX}-settlement/ledger-log-list`, method: 'GET', data: params });
|
return clinicRequest({ url: `${PREFIX}-settlement/ledger-log-list`, method: 'GET', data: params });
|
||||||
}
|
}
|
||||||
|
|||||||
39
api/clinicSalesperson.js
Normal file
39
api/clinicSalesperson.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { req } from '@/common/js/index.js';
|
||||||
|
import { unwrapClinicRes } from '@/api/clinicAdmin.js';
|
||||||
|
|
||||||
|
const PREFIX = '/newApi/clinic-salesperson-self';
|
||||||
|
const AUTH_PREFIX = '/newApi/clinic-salesperson-auth';
|
||||||
|
|
||||||
|
function clinicSalespersonRequest(options) {
|
||||||
|
return req.request({
|
||||||
|
...options,
|
||||||
|
header: {
|
||||||
|
...(options.header || {}),
|
||||||
|
_clinicSalesperson: '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 getClinicSalespersonMyInfo() {
|
||||||
|
return clinicSalespersonRequest({ url: `${AUTH_PREFIX}/my-info`, method: 'GET' });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getClinicSalespersonCommissionList(params) {
|
||||||
|
return clinicSalespersonRequest({ url: `${PREFIX}/commission-list`, method: 'GET', data: params });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getClinicSalespersonSettlementList(params) {
|
||||||
|
return clinicSalespersonRequest({ url: `${PREFIX}/settlement-list`, method: 'GET', data: params });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getClinicSalespersonSettlementDetail(id) {
|
||||||
|
return clinicSalespersonRequest({ url: `${PREFIX}/settlement-detail`, method: 'GET', data: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getClinicSalespersonUserBindList(params) {
|
||||||
|
return clinicSalespersonRequest({ url: `${PREFIX}/user-bind-list`, method: 'GET', data: params });
|
||||||
|
}
|
||||||
5
api/platformAdmin.js
Normal file
5
api/platformAdmin.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { platformAdminApi } from '@/api/businessApi.js';
|
||||||
|
|
||||||
|
export { platformAdminApi };
|
||||||
|
export const getPlatformAdminMyInfo = () => platformAdminApi.getMyInfo();
|
||||||
|
export default platformAdminApi;
|
||||||
5
api/salesperson.js
Normal file
5
api/salesperson.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { salespersonApi } from '@/api/businessApi.js';
|
||||||
|
|
||||||
|
export const getSalespersonMyInfo = salespersonApi.getMyInfo.bind(salespersonApi);
|
||||||
|
export const getSalespersonInputStats = salespersonApi.getInputStats.bind(salespersonApi);
|
||||||
|
export default salespersonApi;
|
||||||
95
api/upload.js
Normal file
95
api/upload.js
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import { req } from '@/common/js/index.js';
|
||||||
|
|
||||||
|
const SALESPERSON_PREFIX = '/newApi/salesperson-upload/';
|
||||||
|
const PLATFORM_PREFIX = '/newApi/platform-admin-upload/';
|
||||||
|
|
||||||
|
function uploadChatFileByPrefix(prefix, headerKey, params) {
|
||||||
|
return req.request({
|
||||||
|
url: `${prefix}upload-chat-file`,
|
||||||
|
type: 'upload',
|
||||||
|
method: 'POST',
|
||||||
|
filePath: params.filePath,
|
||||||
|
name: params.name || 'file',
|
||||||
|
formData: params.formData || {},
|
||||||
|
header: {
|
||||||
|
[headerKey]: '1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 业务员录入/聊天同款:服务端转存 OSS,返回 url
|
||||||
|
*/
|
||||||
|
export function uploadSalespersonChatFileApi(params) {
|
||||||
|
return uploadChatFileByPrefix(SALESPERSON_PREFIX, '_salesperson', params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 平台管理员录入上传(与业务员对称)
|
||||||
|
*/
|
||||||
|
export function uploadPlatformAdminChatFileApi(params) {
|
||||||
|
return uploadChatFileByPrefix(PLATFORM_PREFIX, '_platformAdmin', params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按当前 loginMode 选择上传接口
|
||||||
|
*/
|
||||||
|
export function uploadBusinessChatFileApi(params) {
|
||||||
|
const mode = uni.getStorageSync('loginMode');
|
||||||
|
if (mode === 'platform_admin') {
|
||||||
|
return uploadPlatformAdminChatFileApi(params);
|
||||||
|
}
|
||||||
|
return uploadSalespersonChatFileApi(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
function unwrapUploadResult(res) {
|
||||||
|
if (res == null) return null;
|
||||||
|
if (res.result !== undefined && res.result !== null) return res.result;
|
||||||
|
if (res.data && res.data.result !== undefined) return res.data.result;
|
||||||
|
return res.data != null ? res.data : res;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractUploadUrl(res) {
|
||||||
|
const inner = unwrapUploadResult(res);
|
||||||
|
if (!inner) return null;
|
||||||
|
return inner.url || inner.path || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LEGACY_ADMIN_UPLOAD_URL = 'https://api.xiaokang88.com/open-api/upload/old-admin-img';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 录入/管理端旧版图片上传(与 d-upload、select-avatar-image 一致)
|
||||||
|
*/
|
||||||
|
export function uploadLegacyAdminImage(params) {
|
||||||
|
const { filePath, name = 'file', formData = { store_id: '11001' } } = params || {};
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
uni.uploadFile({
|
||||||
|
url: LEGACY_ADMIN_UPLOAD_URL,
|
||||||
|
filePath,
|
||||||
|
name,
|
||||||
|
formData,
|
||||||
|
header: {
|
||||||
|
authorization: `Bearer ${uni.getStorageSync('token')}`,
|
||||||
|
},
|
||||||
|
success: (uploadRes) => {
|
||||||
|
try {
|
||||||
|
const data =
|
||||||
|
typeof uploadRes.data === 'string' ? JSON.parse(uploadRes.data) : uploadRes.data;
|
||||||
|
if (![200, 201, 204].includes(uploadRes.statusCode)) {
|
||||||
|
reject(new Error((data && data.msg) || '上传失败'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = data && data.data && data.data.url;
|
||||||
|
if (!url) {
|
||||||
|
reject(new Error('上传失败'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(url);
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: reject,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -366,18 +366,24 @@ export default class {
|
|||||||
return new Promise((resolve, rej) => {
|
return new Promise((resolve, rej) => {
|
||||||
uni.canvasToTempFilePath({
|
uni.canvasToTempFilePath({
|
||||||
canvasId: this.canvasName,
|
canvasId: this.canvasName,
|
||||||
|
fileType: 'png',
|
||||||
|
quality: 1,
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
uni.getFileSystemManager().readFile({
|
uni.getFileSystemManager().readFile({
|
||||||
filePath: res.tempFilePath,
|
filePath: res.tempFilePath,
|
||||||
encoding: 'base64',
|
encoding: 'base64',
|
||||||
success: r => {
|
success: (r) => {
|
||||||
resolve(r.data);
|
resolve({
|
||||||
}
|
base64: r.data,
|
||||||
})
|
tempFilePath: res.tempFilePath,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
fail: (err) => rej(err),
|
||||||
|
});
|
||||||
},
|
},
|
||||||
fail: function(err) {
|
fail: (err) => {
|
||||||
rej(err);
|
rej(err);
|
||||||
}
|
},
|
||||||
}, _this)
|
}, _this)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ const arr = [
|
|||||||
'patient_mobile',
|
'patient_mobile',
|
||||||
'id_card',
|
'id_card',
|
||||||
'idcard',
|
'idcard',
|
||||||
'password'
|
'password',
|
||||||
|
'phone'
|
||||||
]
|
]
|
||||||
|
|
||||||
// 敏感数据(解密后做脱敏;需与下方 switch 分支一致)
|
// 敏感数据(解密后做脱敏;需与下方 switch 分支一致)
|
||||||
@@ -26,7 +27,8 @@ const sensitiveData = [
|
|||||||
'idcard',
|
'idcard',
|
||||||
'mobile',
|
'mobile',
|
||||||
'express_mobile',
|
'express_mobile',
|
||||||
'patient_mobile'
|
'patient_mobile',
|
||||||
|
'phone'
|
||||||
]
|
]
|
||||||
/**
|
/**
|
||||||
* 请求拦截器
|
* 请求拦截器
|
||||||
@@ -43,22 +45,50 @@ const reqInterceptor = async (options) => {
|
|||||||
const skipToken = options.header.isToken === false
|
const skipToken = options.header.isToken === false
|
||||||
const isClinicAdminReq = options.header._clinicAdmin === '1'
|
const isClinicAdminReq = options.header._clinicAdmin === '1'
|
||||||
|| (options.url && String(options.url).indexOf('clinic-admin') !== -1)
|
|| (options.url && String(options.url).indexOf('clinic-admin') !== -1)
|
||||||
|
const isPlatformAdminReq = options.header._platformAdmin === '1'
|
||||||
|
|| (options.url && String(options.url).indexOf('platform-admin') !== -1)
|
||||||
|
const isSalespersonReq = options.header._salesperson === '1'
|
||||||
|
|| (options.url && String(options.url).indexOf('salesperson-') !== -1)
|
||||||
|
const isClinicSalespersonReq = options.header._clinicSalesperson === '1'
|
||||||
|
|| (options.url && String(options.url).indexOf('clinic-salesperson-') !== -1)
|
||||||
const isDoctorWxAuthReq = options.url && String(options.url).indexOf('doctor-wx-auth') !== -1
|
const isDoctorWxAuthReq = options.url && String(options.url).indexOf('doctor-wx-auth') !== -1
|
||||||
// 内部控制位,不得作为 HTTP 头发出
|
// 内部控制位,不得作为 HTTP 头发出
|
||||||
delete options.header.isToken
|
delete options.header.isToken
|
||||||
delete options.header._clinicAdmin
|
delete options.header._clinicAdmin
|
||||||
|
delete options.header._platformAdmin
|
||||||
|
delete options.header._salesperson
|
||||||
|
delete options.header._clinicSalesperson
|
||||||
const clinicToken = uni.getStorageSync('clinic_admin_token')
|
const clinicToken = uni.getStorageSync('clinic_admin_token')
|
||||||
|
const platformToken = uni.getStorageSync('platform_admin_token')
|
||||||
|
const salespersonToken = uni.getStorageSync('salesperson_token')
|
||||||
|
const clinicSalespersonToken = uni.getStorageSync('clinic_salesperson_token')
|
||||||
const doctorToken = uni.getStorageSync('token')
|
const doctorToken = uni.getStorageSync('token')
|
||||||
const loginMode = uni.getStorageSync('loginMode')
|
const loginMode = uni.getStorageSync('loginMode')
|
||||||
if (!skipToken) {
|
if (!skipToken) {
|
||||||
if (isDoctorWxAuthReq) {
|
if (isDoctorWxAuthReq) {
|
||||||
if (loginMode === 'clinic_admin' && clinicToken) {
|
if (loginMode === 'platform_admin' && platformToken) {
|
||||||
|
options.header['authorization'] = `Bearer ${platformToken}`
|
||||||
|
} else if (loginMode === 'salesperson' && salespersonToken) {
|
||||||
|
options.header['authorization'] = `Bearer ${salespersonToken}`
|
||||||
|
} else if (loginMode === 'clinic_salesperson' && clinicSalespersonToken) {
|
||||||
|
options.header['authorization'] = `Bearer ${clinicSalespersonToken}`
|
||||||
|
} else if (loginMode === 'clinic_admin' && clinicToken) {
|
||||||
options.header['authorization'] = `Bearer ${clinicToken}`
|
options.header['authorization'] = `Bearer ${clinicToken}`
|
||||||
} else if (doctorToken) {
|
} else if (doctorToken) {
|
||||||
options.header['authorization'] = `Bearer ${doctorToken}`
|
options.header['authorization'] = `Bearer ${doctorToken}`
|
||||||
|
} else if (platformToken) {
|
||||||
|
options.header['authorization'] = `Bearer ${platformToken}`
|
||||||
|
} else if (salespersonToken) {
|
||||||
|
options.header['authorization'] = `Bearer ${salespersonToken}`
|
||||||
} else if (clinicToken) {
|
} else if (clinicToken) {
|
||||||
options.header['authorization'] = `Bearer ${clinicToken}`
|
options.header['authorization'] = `Bearer ${clinicToken}`
|
||||||
}
|
}
|
||||||
|
} else if (isPlatformAdminReq && platformToken) {
|
||||||
|
options.header['authorization'] = `Bearer ${platformToken}`
|
||||||
|
} else if (isSalespersonReq && salespersonToken) {
|
||||||
|
options.header['authorization'] = `Bearer ${salespersonToken}`
|
||||||
|
} else if (isClinicSalespersonReq && clinicSalespersonToken) {
|
||||||
|
options.header['authorization'] = `Bearer ${clinicSalespersonToken}`
|
||||||
} else if (isClinicAdminReq && clinicToken) {
|
} else if (isClinicAdminReq && clinicToken) {
|
||||||
options.header['authorization'] = `Bearer ${clinicToken}`
|
options.header['authorization'] = `Bearer ${clinicToken}`
|
||||||
} else if (doctorToken) {
|
} else if (doctorToken) {
|
||||||
@@ -109,8 +139,20 @@ const resInterceptor = (response, conf = {}) => {
|
|||||||
icon: 'error',
|
icon: 'error',
|
||||||
title: response['msg'] || response['message'] || errorCode[statusCode]
|
title: response['msg'] || response['message'] || errorCode[statusCode]
|
||||||
})
|
})
|
||||||
const isClinic = uni.getStorageSync('loginMode') === 'clinic_admin'
|
const loginMode = uni.getStorageSync('loginMode')
|
||||||
if (isClinic) {
|
if (loginMode === 'platform_admin') {
|
||||||
|
uni.removeStorageSync('platform_admin_token')
|
||||||
|
uni.removeStorageSync('platform_admin_user')
|
||||||
|
uni.removeStorageSync('loginMode')
|
||||||
|
} else if (loginMode === 'salesperson') {
|
||||||
|
uni.removeStorageSync('salesperson_token')
|
||||||
|
uni.removeStorageSync('salesperson_user')
|
||||||
|
uni.removeStorageSync('loginMode')
|
||||||
|
} else if (loginMode === 'clinic_salesperson') {
|
||||||
|
uni.removeStorageSync('clinic_salesperson_token')
|
||||||
|
uni.removeStorageSync('clinic_salesperson_user')
|
||||||
|
uni.removeStorageSync('loginMode')
|
||||||
|
} else if (loginMode === 'clinic_admin') {
|
||||||
uni.removeStorageSync('clinic_admin_token')
|
uni.removeStorageSync('clinic_admin_token')
|
||||||
uni.removeStorageSync('clinic_admin_user')
|
uni.removeStorageSync('clinic_admin_user')
|
||||||
uni.removeStorageSync('loginMode')
|
uni.removeStorageSync('loginMode')
|
||||||
@@ -182,6 +224,7 @@ function getRes(obj, isDecode = true) {
|
|||||||
} else {
|
} else {
|
||||||
// 判断obj[key]是否在arr中
|
// 判断obj[key]是否在arr中
|
||||||
if (arr.indexOf(key) !== -1) {
|
if (arr.indexOf(key) !== -1) {
|
||||||
|
if (obj[key] == null || obj[key] === '') continue
|
||||||
let aseFile = ''
|
let aseFile = ''
|
||||||
if (isDecode === true) {
|
if (isDecode === true) {
|
||||||
aseFile = customBase64Decode(obj[key])
|
aseFile = customBase64Decode(obj[key])
|
||||||
@@ -207,6 +250,7 @@ function getRes(obj, isDecode = true) {
|
|||||||
break;
|
break;
|
||||||
case 'mobile':
|
case 'mobile':
|
||||||
case 'express_mobile':
|
case 'express_mobile':
|
||||||
|
case 'phone':
|
||||||
// 保留前三位和后四位,中间用星号代替
|
// 保留前三位和后四位,中间用星号代替
|
||||||
aseFile = aseFile.slice(0, 3).padEnd(aseFile.length - 4, '*') + aseFile.slice(-4);
|
aseFile = aseFile.slice(0, 3).padEnd(aseFile.length - 4, '*') + aseFile.slice(-4);
|
||||||
break;
|
break;
|
||||||
@@ -233,6 +277,7 @@ function getRes(obj, isDecode = true) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function customBase64Decode(data) {
|
function customBase64Decode(data) {
|
||||||
|
if (data == null || data === '') return data
|
||||||
// return data
|
// return data
|
||||||
try {
|
try {
|
||||||
// 第一次Base64解码
|
// 第一次Base64解码
|
||||||
|
|||||||
@@ -1,112 +1,310 @@
|
|||||||
<template>
|
<template>
|
||||||
|
|
||||||
<view>
|
<view>
|
||||||
<u-popup v-model="show" mode="bottom" :closeable="true" @close="show=false" close-icon-pos='top-right'
|
|
||||||
|
<!-- #ifdef MP-WEIXIN -->
|
||||||
|
|
||||||
|
<page-container
|
||||||
|
|
||||||
|
v-if="usePageContainer && show"
|
||||||
|
|
||||||
|
:show="show"
|
||||||
|
|
||||||
|
:overlay="false"
|
||||||
|
|
||||||
|
@leave="close"
|
||||||
|
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- #endif -->
|
||||||
|
|
||||||
|
<u-popup v-model="show" mode="bottom" :closeable="true" @close="close" close-icon-pos='top-right'
|
||||||
|
|
||||||
:safe-area-inset-bottom="true" :mask-close-able="false">
|
:safe-area-inset-bottom="true" :mask-close-able="false">
|
||||||
|
|
||||||
<view class="handwritten flex-col flex-ali-center">
|
<view class="handwritten flex-col flex-ali-center">
|
||||||
|
|
||||||
<text class="title">签字确认</text>
|
<text class="title">签字确认</text>
|
||||||
|
|
||||||
<view class="">
|
<view class="">
|
||||||
|
|
||||||
<canvas :style="{width: '686rpx',height: '480rpx',border:'1px solid #2979FF'}" disable-scroll="true"
|
<canvas :style="{width: '686rpx',height: '480rpx',border:'1px solid #2979FF'}" disable-scroll="true"
|
||||||
|
|
||||||
data-id="1" @touchstart="penStart" @touchmove="penMove" @touchend="penEnd"
|
data-id="1" @touchstart="penStart" @touchmove="penMove" @touchend="penEnd"
|
||||||
|
|
||||||
canvas-id="canvas"></canvas>
|
canvas-id="canvas"></canvas>
|
||||||
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="flex-row" style="margin-top: 80rpx;">
|
<view class="flex-row" style="margin-top: 80rpx;">
|
||||||
|
|
||||||
<view class="retDraw" @click="retDraw">重写</view>
|
<view class="retDraw" @click="retDraw">重写</view>
|
||||||
|
|
||||||
<view style="width: 343rpx;height: 86rpx;">
|
<view style="width: 343rpx;height: 86rpx;">
|
||||||
<u-button :throttle-time="0" @click="saveCanvas();loading=true" type="primary" :loading="loading"
|
|
||||||
|
<u-button :throttle-time="0" @click="onConfirm" type="primary" :loading="loading"
|
||||||
|
|
||||||
:custom-style="{borderRadius:'0'}">确定</u-button>
|
:custom-style="{borderRadius:'0'}">确定</u-button>
|
||||||
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
</u-popup>
|
</u-popup>
|
||||||
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
import Mycanvas from "@/common/js/handwriting.js";
|
import Mycanvas from "@/common/js/handwriting.js";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
||||||
name: 'd-handwritten',
|
name: 'd-handwritten',
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
// 是否显示d-handwritten
|
|
||||||
value: {
|
value: {
|
||||||
|
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
|
||||||
default: false
|
default: false
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
usePageContainer: {
|
||||||
|
|
||||||
|
type: Boolean,
|
||||||
|
|
||||||
|
default: false
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
||||||
show: this.value,
|
show: this.value,
|
||||||
canvas: '',
|
|
||||||
|
canvas: null,
|
||||||
|
|
||||||
loading: false
|
loading: false
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
value() {
|
|
||||||
this.show = this.value
|
value(v) {
|
||||||
}
|
|
||||||
|
this.show = v
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
show(v) {
|
||||||
|
|
||||||
|
this.$emit('input', v)
|
||||||
|
|
||||||
|
if (v) {
|
||||||
|
|
||||||
|
this.$nextTick(() => this.initCanvas())
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
},
|
},
|
||||||
methods: {
|
|
||||||
// 清除画布
|
mounted() {
|
||||||
retDraw() {
|
|
||||||
this.canvas.retDraw();
|
|
||||||
},
|
|
||||||
// 笔迹开始
|
|
||||||
penStart(event) {
|
|
||||||
this.canvas.penStart(event);
|
|
||||||
},
|
|
||||||
// 笔迹移动
|
|
||||||
penMove(event) {
|
|
||||||
this.canvas.penMove(event);
|
|
||||||
},
|
|
||||||
// 笔迹结束
|
|
||||||
penEnd(event) {
|
|
||||||
this.canvas.penEnd(event);
|
|
||||||
},
|
|
||||||
// 保存
|
|
||||||
async saveCanvas() {
|
|
||||||
let pic = await this.canvas.saveCanvas(this);
|
|
||||||
this.loading = false;
|
|
||||||
this.$emit('saveCanvas', pic)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onReady() {
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.canvas = new Mycanvas({
|
|
||||||
lineColor: this.lineColor,
|
if (this.show) this.initCanvas()
|
||||||
lineSise: this.lineSise,
|
|
||||||
canvasName: "canvas"
|
|
||||||
}, this)
|
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
|
||||||
|
initCanvas() {
|
||||||
|
|
||||||
|
this.canvas = new Mycanvas({
|
||||||
|
|
||||||
|
lineColor: '#1A1A1A',
|
||||||
|
|
||||||
|
lineSise: 50,
|
||||||
|
|
||||||
|
canvasName: 'canvas',
|
||||||
|
|
||||||
|
}, this)
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
close() {
|
||||||
|
|
||||||
|
this.show = false
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
retDraw() {
|
||||||
|
|
||||||
|
if (this.canvas && typeof this.canvas.retDraw === 'function') {
|
||||||
|
|
||||||
|
this.canvas.retDraw()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
penStart(event) {
|
||||||
|
|
||||||
|
if (!this.canvas) return
|
||||||
|
|
||||||
|
this.canvas.penStart(event)
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
penMove(event) {
|
||||||
|
|
||||||
|
if (!this.canvas) return
|
||||||
|
|
||||||
|
this.canvas.penMove(event)
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
penEnd(event) {
|
||||||
|
|
||||||
|
if (!this.canvas) return
|
||||||
|
|
||||||
|
this.canvas.penEnd(event)
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
onConfirm() {
|
||||||
|
|
||||||
|
this.loading = true
|
||||||
|
|
||||||
|
this.saveCanvas()
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveCanvas() {
|
||||||
|
|
||||||
|
if (!this.canvas || typeof this.canvas.saveCanvas !== 'function') {
|
||||||
|
|
||||||
|
this.loading = false
|
||||||
|
|
||||||
|
uni.showToast({ title: '签名板未就绪', icon: 'none' })
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
const pic = await this.canvas.saveCanvas(this)
|
||||||
|
|
||||||
|
this.loading = false
|
||||||
|
|
||||||
|
const base64 = typeof pic === 'string' ? pic : (pic && pic.base64)
|
||||||
|
|
||||||
|
if (!base64 && !(pic && pic.tempFilePath)) {
|
||||||
|
|
||||||
|
uni.showToast({ title: '请先签名', icon: 'none' })
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
this.show = false
|
||||||
|
|
||||||
|
this.$emit('saveCanvas', pic)
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
|
||||||
|
this.loading = false
|
||||||
|
|
||||||
|
uni.showToast({ title: '签名导出失败', icon: 'none' })
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
.handwritten {
|
.handwritten {
|
||||||
|
|
||||||
width: 750rpx;
|
width: 750rpx;
|
||||||
|
|
||||||
background: #FFFFFF;
|
background: #FFFFFF;
|
||||||
|
|
||||||
border-radius: 16rpx 16rpx 0px 0px;
|
border-radius: 16rpx 16rpx 0px 0px;
|
||||||
|
|
||||||
height: calc(840rpx + env(safe-area-inset-bottom));
|
height: calc(840rpx + env(safe-area-inset-bottom));
|
||||||
|
|
||||||
padding-bottom: env(safe-area-inset-bottom);
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
|
|
||||||
font-size: 32rpx;
|
font-size: 32rpx;
|
||||||
|
|
||||||
font-family: PingFang SC-Regular, PingFang SC;
|
font-family: PingFang SC-Regular, PingFang SC;
|
||||||
|
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
|
||||||
color: #323233;
|
color: #323233;
|
||||||
|
|
||||||
margin-top: 30rpx;
|
margin-top: 30rpx;
|
||||||
|
|
||||||
margin-bottom: 48rpx;
|
margin-bottom: 48rpx;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.retDraw {
|
.retDraw {
|
||||||
|
|
||||||
width: 343rpx;
|
width: 343rpx;
|
||||||
|
|
||||||
height: 86rpx;
|
height: 86rpx;
|
||||||
|
|
||||||
background: #FFFFFF rgba(41, 121, 255, 0);
|
background: #FFFFFF rgba(41, 121, 255, 0);
|
||||||
|
|
||||||
font-size: 32rpx;
|
font-size: 32rpx;
|
||||||
|
|
||||||
font-family: PingFang SC-Medium, PingFang SC;
|
font-family: PingFang SC-Medium, PingFang SC;
|
||||||
|
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|
||||||
color: #2979FF;
|
color: #2979FF;
|
||||||
|
|
||||||
line-height: 86rpx;
|
line-height: 86rpx;
|
||||||
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
50
pages.json
50
pages.json
@@ -453,6 +453,56 @@
|
|||||||
"style": { "navigationBarTitleText": "药品详情", "navigationStyle": "custom" }
|
"style": { "navigationBarTitleText": "药品详情", "navigationStyle": "custom" }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
}, {
|
||||||
|
"root": "subPackages/sub_business_shared",
|
||||||
|
"pages": [
|
||||||
|
{ "path": "order/index", "style": { "navigationBarTitleText": "商品订单", "navigationStyle": "custom" } },
|
||||||
|
{ "path": "order/detail", "style": { "navigationBarTitleText": "订单详情", "navigationStyle": "custom" } },
|
||||||
|
{ "path": "order/trace/index", "style": { "navigationBarTitleText": "订单追溯", "navigationStyle": "custom" } },
|
||||||
|
{ "path": "reconciliation/index", "style": { "navigationBarTitleText": "对账单", "navigationStyle": "custom" } },
|
||||||
|
{ "path": "withdrawal/index", "style": { "navigationBarTitleText": "提现管理", "navigationStyle": "custom" } },
|
||||||
|
{ "path": "withdrawal/apply", "style": { "navigationBarTitleText": "申请提现", "navigationStyle": "custom" } },
|
||||||
|
{ "path": "withdrawal/card-edit", "style": { "navigationBarTitleText": "编辑账户", "navigationStyle": "custom" } },
|
||||||
|
{ "path": "withdrawal/record-detail", "style": { "navigationBarTitleText": "提现详情", "navigationStyle": "custom" } },
|
||||||
|
{ "path": "withdrawal/settlement/index", "style": { "navigationBarTitleText": "结算记录", "navigationStyle": "custom" } },
|
||||||
|
{ "path": "withdrawal/settlement/detail", "style": { "navigationBarTitleText": "结算明细", "navigationStyle": "custom" } },
|
||||||
|
{ "path": "withdrawal/account-change/index", "style": { "navigationBarTitleText": "资金变动", "navigationStyle": "custom" } },
|
||||||
|
{ "path": "warehouse/index", "style": { "navigationBarTitleText": "仓库管理", "navigationStyle": "custom" } },
|
||||||
|
{ "path": "warehouse/detail", "style": { "navigationBarTitleText": "药品详情", "navigationStyle": "custom" } }
|
||||||
|
]
|
||||||
|
}, {
|
||||||
|
"root": "subPackages/sub_platform_admin",
|
||||||
|
"pages": [
|
||||||
|
{ "path": "home/index", "style": { "navigationBarTitleText": "工作台", "navigationStyle": "custom" } },
|
||||||
|
{ "path": "my/index", "style": { "navigationBarTitleText": "我的", "navigationStyle": "custom" } },
|
||||||
|
{ "path": "withdrawal/audit/index", "style": { "navigationBarTitleText": "提现审核", "navigationStyle": "custom" } },
|
||||||
|
{ "path": "store-input/audit/index", "style": { "navigationBarTitleText": "门店审核", "navigationStyle": "custom" } },
|
||||||
|
{ "path": "store-input/audit/detail", "style": { "navigationBarTitleText": "审核详情", "navigationStyle": "custom" } },
|
||||||
|
{ "path": "doctor-input/audit/index", "style": { "navigationBarTitleText": "医生审核", "navigationStyle": "custom" } },
|
||||||
|
{ "path": "doctor-input/audit/detail", "style": { "navigationBarTitleText": "审核详情", "navigationStyle": "custom" } }
|
||||||
|
]
|
||||||
|
}, {
|
||||||
|
"root": "subPackages/sub_salesperson",
|
||||||
|
"pages": [
|
||||||
|
{ "path": "home/index", "style": { "navigationBarTitleText": "工作台", "navigationStyle": "custom" } },
|
||||||
|
{ "path": "my/index", "style": { "navigationBarTitleText": "我的", "navigationStyle": "custom" } },
|
||||||
|
{ "path": "store-input/index", "style": { "navigationBarTitleText": "列表", "navigationStyle": "custom" } },
|
||||||
|
{ "path": "store-input/form", "style": { "navigationBarTitleText": "录入", "navigationStyle": "custom" } },
|
||||||
|
{ "path": "store-input/detail", "style": { "navigationBarTitleText": "录入详情", "navigationStyle": "custom" } },
|
||||||
|
{ "path": "doctor-input/index", "style": { "navigationBarTitleText": "列表", "navigationStyle": "custom" } },
|
||||||
|
{ "path": "doctor-input/form", "style": { "navigationBarTitleText": "录入", "navigationStyle": "custom" } },
|
||||||
|
{ "path": "doctor-input/detail", "style": { "navigationBarTitleText": "预填详情", "navigationStyle": "custom" } }
|
||||||
|
]
|
||||||
|
}, {
|
||||||
|
"root": "subPackages/sub_clinic_salesperson",
|
||||||
|
"pages": [
|
||||||
|
{ "path": "home/index", "style": { "navigationBarTitleText": "工作台", "navigationStyle": "custom" } },
|
||||||
|
{ "path": "my/index", "style": { "navigationBarTitleText": "我的", "navigationStyle": "custom" } },
|
||||||
|
{ "path": "earnings/index", "style": { "navigationBarTitleText": "我的收益", "navigationStyle": "custom" } },
|
||||||
|
{ "path": "leads/index", "style": { "navigationBarTitleText": "获客记录", "navigationStyle": "custom" } },
|
||||||
|
{ "path": "commission/index", "style": { "navigationBarTitleText": "分成记录", "navigationStyle": "custom" } },
|
||||||
|
{ "path": "settlement/index", "style": { "navigationBarTitleText": "结算记录", "navigationStyle": "custom" } }
|
||||||
|
]
|
||||||
}, {
|
}, {
|
||||||
"root": "subPackages/sub_agreement",
|
"root": "subPackages/sub_agreement",
|
||||||
"pages": [{
|
"pages": [{
|
||||||
|
|||||||
@@ -12,25 +12,12 @@
|
|||||||
<script>
|
<script>
|
||||||
import DoctorLogin from '@/components/doctor-login/doctor-login.vue';
|
import DoctorLogin from '@/components/doctor-login/doctor-login.vue';
|
||||||
import AccountSwitchPanel from '@/components/account-switch/account-switch-panel.vue';
|
import AccountSwitchPanel from '@/components/account-switch/account-switch-panel.vue';
|
||||||
|
import { redirectIfLoggedIn } from '@/utils/loginSession.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { DoctorLogin, AccountSwitchPanel },
|
components: { DoctorLogin, AccountSwitchPanel },
|
||||||
onShow() {
|
onShow() {
|
||||||
const clinicToken = uni.getStorageSync('clinic_admin_token');
|
redirectIfLoggedIn();
|
||||||
const token = uni.getStorageSync('token');
|
|
||||||
const loginMode = uni.getStorageSync('loginMode');
|
|
||||||
if (loginMode === 'clinic_admin' && clinicToken) {
|
|
||||||
uni.reLaunch({ url: '/subPackages/sub_clinic_admin/home/index' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (token) {
|
|
||||||
const role = Number(uni.getStorageSync('identity') || uni.getStorageSync('role') || 0);
|
|
||||||
if (role === 2) {
|
|
||||||
uni.reLaunch({ url: '/pages/pharmacist/index' });
|
|
||||||
} else {
|
|
||||||
uni.reLaunch({ url: '/pages/workbench/index' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
openAccountPicker({ accounts, defaultAccountId, defaultAccountType }) {
|
openAccountPicker({ accounts, defaultAccountId, defaultAccountType }) {
|
||||||
|
|||||||
26
subPackages/sub_business_shared/common/businessMixin.js
Normal file
26
subPackages/sub_business_shared/common/businessMixin.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { resolveBusinessContext, pageUrl, businessPageRoot } from './context.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
inject: {
|
||||||
|
businessContext: { default: null },
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
bizCtx() {
|
||||||
|
return resolveBusinessContext(this.businessContext);
|
||||||
|
},
|
||||||
|
bizApi() {
|
||||||
|
return this.bizCtx.api;
|
||||||
|
},
|
||||||
|
bizCap() {
|
||||||
|
return this.bizCtx.capabilities || {};
|
||||||
|
},
|
||||||
|
bizRoot() {
|
||||||
|
return businessPageRoot(this.bizCtx);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
bizPage(subPath) {
|
||||||
|
return pageUrl(this.bizCtx, subPath);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
102
subPackages/sub_business_shared/common/context.js
Normal file
102
subPackages/sub_business_shared/common/context.js
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import { platformAdminApi, salespersonApi } from '@/api/businessApi.js';
|
||||||
|
import * as clinicApi from '@/api/clinicAdmin.js';
|
||||||
|
|
||||||
|
const BUSINESS_PACKAGE_ROOT = '/subPackages/sub_business_shared';
|
||||||
|
const PLATFORM_HOME = '/subPackages/sub_platform_admin/home/index';
|
||||||
|
const PLATFORM_MY = '/subPackages/sub_platform_admin/my/index';
|
||||||
|
const SALESPERSON_HOME = '/subPackages/sub_salesperson/home/index';
|
||||||
|
const SALESPERSON_MY = '/subPackages/sub_salesperson/my/index';
|
||||||
|
|
||||||
|
export function getPlatformAdminContext() {
|
||||||
|
return {
|
||||||
|
mode: 'platform_admin',
|
||||||
|
api: platformAdminApi,
|
||||||
|
userStorageKey: 'platform_admin_user',
|
||||||
|
homePath: PLATFORM_HOME,
|
||||||
|
myPath: PLATFORM_MY,
|
||||||
|
packageRoot: '/subPackages/sub_platform_admin',
|
||||||
|
businessPackageRoot: BUSINESS_PACKAGE_ROOT,
|
||||||
|
brandTitle: '平台管理工作台',
|
||||||
|
capabilities: {
|
||||||
|
canShip: true,
|
||||||
|
canRefund: true,
|
||||||
|
canStoreFilter: true,
|
||||||
|
canWithdraw: true,
|
||||||
|
canWithdrawAudit: true,
|
||||||
|
canWarehouse: true,
|
||||||
|
warehouseType: 'platform',
|
||||||
|
canStoreInputAudit: true,
|
||||||
|
canDoctorInputAudit: true,
|
||||||
|
},
|
||||||
|
tabList: [
|
||||||
|
{ text: '工作台', iconPath: '/static/tabbar/home.png', selectedIconPath: '/static/tabbar/home-active.png', pagePath: PLATFORM_HOME },
|
||||||
|
{ text: '我的', iconPath: '/static/tabbar/my.png', selectedIconPath: '/static/tabbar/my-active.png', pagePath: PLATFORM_MY },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSalespersonContext() {
|
||||||
|
return {
|
||||||
|
mode: 'salesperson',
|
||||||
|
api: salespersonApi,
|
||||||
|
userStorageKey: 'salesperson_user',
|
||||||
|
homePath: SALESPERSON_HOME,
|
||||||
|
myPath: SALESPERSON_MY,
|
||||||
|
packageRoot: '/subPackages/sub_salesperson',
|
||||||
|
businessPackageRoot: BUSINESS_PACKAGE_ROOT,
|
||||||
|
brandTitle: '业务工作台',
|
||||||
|
capabilities: {
|
||||||
|
canShip: false,
|
||||||
|
canRefund: false,
|
||||||
|
canStoreFilter: true,
|
||||||
|
canStoreInput: true,
|
||||||
|
canDoctorInput: true,
|
||||||
|
},
|
||||||
|
tabList: [
|
||||||
|
{ text: '工作台', iconPath: '/static/tabbar/home.png', selectedIconPath: '/static/tabbar/home-active.png', pagePath: SALESPERSON_HOME },
|
||||||
|
{ text: '我的', iconPath: '/static/tabbar/my.png', selectedIconPath: '/static/tabbar/my-active.png', pagePath: SALESPERSON_MY },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getClinicAdminContext() {
|
||||||
|
return {
|
||||||
|
mode: 'clinic_admin',
|
||||||
|
api: clinicApi,
|
||||||
|
userStorageKey: 'clinic_admin_user',
|
||||||
|
homePath: '/subPackages/sub_clinic_admin/home/index',
|
||||||
|
myPath: '/subPackages/sub_clinic_admin/my/index',
|
||||||
|
packageRoot: '/subPackages/sub_clinic_admin',
|
||||||
|
businessPackageRoot: '/subPackages/sub_clinic_admin',
|
||||||
|
brandTitle: '诊所管理系统',
|
||||||
|
capabilities: {
|
||||||
|
canShip: false,
|
||||||
|
canRefund: false,
|
||||||
|
canStoreFilter: false,
|
||||||
|
canWithdraw: true,
|
||||||
|
canWithdrawAudit: false,
|
||||||
|
canWarehouse: true,
|
||||||
|
warehouseType: 'store',
|
||||||
|
},
|
||||||
|
tabList: [
|
||||||
|
{ text: '工作台', iconPath: '/static/tabbar/home.png', selectedIconPath: '/static/tabbar/home-active.png', pagePath: '/subPackages/sub_clinic_admin/home/index' },
|
||||||
|
{ text: '我的', iconPath: '/static/tabbar/my.png', selectedIconPath: '/static/tabbar/my-active.png', pagePath: '/subPackages/sub_clinic_admin/my/index' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveBusinessContext(injected) {
|
||||||
|
if (injected && injected.api) return injected;
|
||||||
|
const mode = uni.getStorageSync('loginMode');
|
||||||
|
if (mode === 'platform_admin') return getPlatformAdminContext();
|
||||||
|
if (mode === 'salesperson') return getSalespersonContext();
|
||||||
|
return getClinicAdminContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function businessPageRoot(ctx) {
|
||||||
|
return ctx.businessPackageRoot || ctx.packageRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pageUrl(ctx, subPath) {
|
||||||
|
return `${businessPageRoot(ctx)}/${subPath}`;
|
||||||
|
}
|
||||||
540
subPackages/sub_business_shared/common/display.js
Normal file
540
subPackages/sub_business_shared/common/display.js
Normal file
@@ -0,0 +1,540 @@
|
|||||||
|
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 {}
|
||||||
|
const isIncrease = item.merged
|
||||||
|
? (item.type_txt === '增加' || Number(item.amount) >= 0)
|
||||||
|
: item.type === 1
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
orderNo: item.order_no || (item.order_id ? '订单#' + item.order_id : '-'),
|
||||||
|
orderTypeText: item.order_type_txt || '',
|
||||||
|
feeTypeText: item.fee_type_txt || '-',
|
||||||
|
amountText: formatMoney(item.amount),
|
||||||
|
amountClass: isIncrease ? 'text-success' : 'text-danger',
|
||||||
|
typeText: item.type_txt || '-',
|
||||||
|
content: item.content || '',
|
||||||
|
createdAt: formatDateTime(item.created_at),
|
||||||
|
merged: !!item.merged,
|
||||||
|
orderId: item.order_id,
|
||||||
|
orderType: item.order_type,
|
||||||
|
_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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 订单追溯对账汇总(summary 字段,对齐 mapReconciliationStoreRow 结构) */
|
||||||
|
export function mapReconciliationOrderSummary(summary) {
|
||||||
|
if (!summary) return null
|
||||||
|
return {
|
||||||
|
totalSales: formatMoney(summary.total_sales_price),
|
||||||
|
totalSupply: formatMoney(summary.total_supply_price),
|
||||||
|
registrationPrice: formatMoney(summary.registration_price),
|
||||||
|
registerOrderNo: summary.register_order_no || '',
|
||||||
|
herbal: mapReconciliationCategory(summary, 'herbal'),
|
||||||
|
medicine: mapReconciliationCategory(summary, 'medicine'),
|
||||||
|
servicePackage: mapReconciliationCategory(summary, 'service_package'),
|
||||||
|
otherFees: {
|
||||||
|
express: formatMoney(summary.express_price),
|
||||||
|
process: formatMoney(summary.process_price),
|
||||||
|
treatment: formatMoney(summary.treatment_price),
|
||||||
|
},
|
||||||
|
_raw: summary,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) : '-' },
|
||||||
|
]
|
||||||
|
}
|
||||||
122
subPackages/sub_business_shared/common/filterCache.js
Normal file
122
subPackages/sub_business_shared/common/filterCache.js
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
/**
|
||||||
|
* 诊所端列表筛选条件本地缓存(对齐 PC reconciliation formCache)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
defaultMonthStartDate,
|
||||||
|
defaultTodayDate,
|
||||||
|
} from './reconciliationParams.js'
|
||||||
|
|
||||||
|
const KEY_PREFIX = () => uni.getStorageSync('loginMode') || 'clinic_admin'
|
||||||
|
const KEY_ORDER = () => `${KEY_PREFIX()}.order.filter`
|
||||||
|
const KEY_RECONCILIATION = () => `${KEY_PREFIX()}.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_business_shared/common/format.js
Normal file
7
subPackages/sub_business_shared/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_business_shared/common/orderParams.js
Normal file
32
subPackages/sub_business_shared/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)
|
||||||
|
}
|
||||||
@@ -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_business_shared/common/tabbar.js
Normal file
28
subPackages/sub_business_shared/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>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page-sticky-header">
|
||||||
|
<slot />
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import businessMixin from '@/subPackages/sub_business_shared/common/businessMixin.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [businessMixin],
|
||||||
|
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,80 @@
|
|||||||
|
<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 businessMixin from '@/subPackages/sub_business_shared/common/businessMixin.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'BusinessPageLayout',
|
||||||
|
mixins: [businessMixin],
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
tabList() {
|
||||||
|
return this.bizCtx.tabList || [];
|
||||||
|
},
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onTabChange(index) {
|
||||||
|
if (index === this.tabIndex) return;
|
||||||
|
const target = this.tabList[index];
|
||||||
|
if (target && target.pagePath) {
|
||||||
|
uni.reLaunch({ url: target.pagePath });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.clinic-layout {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f9fafb;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
<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 '@/subPackages/sub_business_shared/common/tabbar.js'
|
||||||
|
|
||||||
|
import businessMixin from '@/subPackages/sub_business_shared/common/businessMixin.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [businessMixin],
|
||||||
|
name: 'BusinessPageLayout',
|
||||||
|
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>
|
||||||
81
subPackages/sub_business_shared/input/InputDetailPage.vue
Normal file
81
subPackages/sub_business_shared/input/InputDetailPage.vue
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<template>
|
||||||
|
<business-page-layout title="录入详情" :show-back="true">
|
||||||
|
<view class="detail-wrap" v-if="info.id">
|
||||||
|
<view class="row" v-for="f in fields" :key="f.k"><text class="k">{{ f.l }}</text><text class="v">{{ display(f.k) }}</text></view>
|
||||||
|
<view v-if="canEdit" class="actions">
|
||||||
|
<button @click="goEdit">继续编辑</button>
|
||||||
|
</view>
|
||||||
|
<view v-if="canAudit" class="actions">
|
||||||
|
<button class="ok" @click="audit(1)">审核通过</button>
|
||||||
|
<button class="no" @click="audit(2)">审核拒绝</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</business-page-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import BusinessPageLayout from '@/subPackages/sub_business_shared/components/business-page-layout.vue'
|
||||||
|
import businessMixin from '@/subPackages/sub_business_shared/common/businessMixin.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [businessMixin],
|
||||||
|
components: { BusinessPageLayout },
|
||||||
|
props: {
|
||||||
|
inputType: { type: String, default: 'store' },
|
||||||
|
editPath: { type: String, default: '' },
|
||||||
|
},
|
||||||
|
data() { return { id: 0, info: {} } },
|
||||||
|
computed: {
|
||||||
|
fields() {
|
||||||
|
if (this.inputType === 'doctor') {
|
||||||
|
return [
|
||||||
|
{ k: 'name', l: '姓名' }, { k: 'mobile', l: '手机' }, { k: 'depart_id', l: '科室ID' },
|
||||||
|
{ k: 'good_at', l: '擅长' }, { k: 'intro', l: '简介' },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{ k: 'name', l: '名称' }, { k: 'mobile', l: '电话' }, { k: 'contact', l: '联系人' },
|
||||||
|
{ k: 'position', l: '地址' }, { k: 'erp_id', l: 'ERP ID' },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
canEdit() { return !this.canAudit && Number(this.info.status) === 0 },
|
||||||
|
canAudit() {
|
||||||
|
if (this.inputType === 'doctor') return !!this.bizCap.canDoctorInputAudit
|
||||||
|
return !!this.bizCap.canStoreInputAudit
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onLoad(q) { this.id = Number(q.id || 0); this.load() },
|
||||||
|
methods: {
|
||||||
|
detailApi() {
|
||||||
|
return this.inputType === 'doctor'
|
||||||
|
? this.bizApi.getDoctorInputDetail(this.id)
|
||||||
|
: this.bizApi.getStoreInputDetail(this.id)
|
||||||
|
},
|
||||||
|
auditApi(data) {
|
||||||
|
return this.inputType === 'doctor'
|
||||||
|
? this.bizApi.auditDoctorInput(data)
|
||||||
|
: this.bizApi.auditStoreInput(data)
|
||||||
|
},
|
||||||
|
load() {
|
||||||
|
this.detailApi().then(w => { if (w.ok) this.info = w.data || {} })
|
||||||
|
},
|
||||||
|
display(k) { return this.info[k] != null && this.info[k] !== '' ? this.info[k] : '-' },
|
||||||
|
goEdit() { if (this.editPath) uni.navigateTo({ url: `${this.editPath}?id=${this.id}` }) },
|
||||||
|
audit(status) {
|
||||||
|
this.auditApi({ id: this.id, status, audit_remark: '' }).then(w => {
|
||||||
|
if (w.ok) { uni.showToast({ title: '操作成功' }); setTimeout(() => uni.navigateBack(), 500) }
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.detail-wrap { padding: 24rpx; background: #fff; min-height: 100vh; }
|
||||||
|
.row { display: flex; padding: 24rpx 0; border-bottom: 1rpx solid #F2F3F5; }
|
||||||
|
.k { width: 180rpx; color: #86909C; }
|
||||||
|
.v { flex: 1; }
|
||||||
|
.actions { display: flex; gap: 16rpx; margin-top: 40rpx; }
|
||||||
|
.ok { flex: 1; background: #6acdbb; color: #fff; }
|
||||||
|
.no { flex: 1; background: #F53F3F; color: #fff; }
|
||||||
|
</style>
|
||||||
86
subPackages/sub_business_shared/input/InputListPage.vue
Normal file
86
subPackages/sub_business_shared/input/InputListPage.vue
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
<template>
|
||||||
|
<business-page-layout :title="title" :show-back="true">
|
||||||
|
<view class="page-wrap">
|
||||||
|
<view v-if="!auditMode" class="toolbar">
|
||||||
|
<button class="btn" @click="goForm()">新建录入</button>
|
||||||
|
</view>
|
||||||
|
<view v-for="item in list" :key="item.id" class="card" @click="goDetail(item)">
|
||||||
|
<view class="row">
|
||||||
|
<text class="name">{{ item.name }}</text>
|
||||||
|
<text class="status" :class="'s' + item.status">{{ statusText(item.status) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="sub">{{ item.mobile }} · {{ formatTime(item.created_at) }}</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="!list.length" class="empty">暂无记录</view>
|
||||||
|
</view>
|
||||||
|
</business-page-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import BusinessPageLayout from '@/subPackages/sub_business_shared/components/business-page-layout.vue'
|
||||||
|
import businessMixin from '@/subPackages/sub_business_shared/common/businessMixin.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [businessMixin],
|
||||||
|
components: { BusinessPageLayout },
|
||||||
|
props: {
|
||||||
|
auditMode: { type: Boolean, default: false },
|
||||||
|
inputType: { type: String, default: 'store' },
|
||||||
|
formPath: { type: String, required: true },
|
||||||
|
detailPath: { type: String, required: true },
|
||||||
|
},
|
||||||
|
data() { return { list: [], page: 1 } },
|
||||||
|
computed: {
|
||||||
|
title() {
|
||||||
|
if (this.inputType === 'doctor') return this.auditMode ? '医生信息审核' : '医生信息预填'
|
||||||
|
return this.auditMode ? '诊所信息审核' : '诊所信息录入'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onShow() { this.reload() },
|
||||||
|
onReachBottom() { this.load(this.page + 1, true) },
|
||||||
|
methods: {
|
||||||
|
listApi(params) {
|
||||||
|
return this.inputType === 'doctor'
|
||||||
|
? this.bizApi.getDoctorInputList(params)
|
||||||
|
: this.bizApi.getStoreInputList(params)
|
||||||
|
},
|
||||||
|
reload() { this.page = 1; this.load(1, false) },
|
||||||
|
load(page, append) {
|
||||||
|
const params = { page, pageSize: 15 }
|
||||||
|
if (this.auditMode) params.status = 0
|
||||||
|
this.listApi(params).then(w => {
|
||||||
|
const items = (w.data && w.data.items) || []
|
||||||
|
this.list = append ? this.list.concat(items) : items
|
||||||
|
this.page = page
|
||||||
|
})
|
||||||
|
},
|
||||||
|
statusText(s) {
|
||||||
|
return { 0: '待审核', 1: '已通过', 2: '已拒绝' }[Number(s)] || '-'
|
||||||
|
},
|
||||||
|
formatTime(t) {
|
||||||
|
if (!t) return '-'
|
||||||
|
const d = new Date(Number(t) * 1000)
|
||||||
|
return `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()}`
|
||||||
|
},
|
||||||
|
goForm() { uni.navigateTo({ url: this.formPath }) },
|
||||||
|
goDetail(item) {
|
||||||
|
uni.navigateTo({ url: `${this.detailPath}?id=${item.id}` })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page-wrap { padding: 24rpx; }
|
||||||
|
.toolbar { margin-bottom: 24rpx; }
|
||||||
|
.btn { background: #6acdbb; color: #fff; font-size: 28rpx; }
|
||||||
|
.card { background: #fff; padding: 28rpx; border-radius: 16rpx; margin-bottom: 16rpx; }
|
||||||
|
.row { display: flex; justify-content: space-between; align-items: center; }
|
||||||
|
.name { font-size: 30rpx; font-weight: 600; }
|
||||||
|
.status { font-size: 24rpx; }
|
||||||
|
.s0 { color: #FF7D00; }
|
||||||
|
.s1 { color: #00B42A; }
|
||||||
|
.s2 { color: #F53F3F; }
|
||||||
|
.sub { margin-top: 12rpx; color: #86909C; font-size: 24rpx; }
|
||||||
|
.empty { text-align: center; color: #86909C; padding: 80rpx 0; }
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
<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 '@/subPackages/sub_business_shared/common/display.js'
|
||||||
|
|
||||||
|
import businessMixin from '@/subPackages/sub_business_shared/common/businessMixin.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [businessMixin],
|
||||||
|
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,593 @@
|
|||||||
|
<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 { mapPrescriptionDetailView } from '@/subPackages/sub_business_shared/common/display.js'
|
||||||
|
|
||||||
|
import businessMixin from '@/subPackages/sub_business_shared/common/businessMixin.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [businessMixin],
|
||||||
|
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
|
||||||
|
this.bizApi.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>
|
||||||
587
subPackages/sub_business_shared/order/detail.vue
Normal file
587
subPackages/sub_business_shared/order/detail.vue
Normal file
@@ -0,0 +1,587 @@
|
|||||||
|
<template>
|
||||||
|
<business-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">
|
||||||
|
<button class="btn-outline" @click="goTrace">订单追溯</button>
|
||||||
|
<button v-if="canShip" class="btn-primary" @click="ship">发货</button>
|
||||||
|
<button v-if="canRefund" class="btn-warn" @click="refund">退款</button>
|
||||||
|
<button v-if="detailExtra.canViewPrescription" class="btn-primary btn-full" @click="openPrescription">查看处方</button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 处方弹窗 -->
|
||||||
|
<prescription-detail-popup ref="prescriptionPopup" />
|
||||||
|
</business-page-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import BusinessPageLayout from '@/subPackages/sub_business_shared/components/business-page-layout.vue'
|
||||||
|
import ExpressTimeline from './components/ExpressTimeline.vue'
|
||||||
|
import PrescriptionDetailPopup from './components/PrescriptionDetailPopup.vue'
|
||||||
|
import {
|
||||||
|
orderStatusText,
|
||||||
|
deliveryMethodText,
|
||||||
|
orderOnlineText,
|
||||||
|
prescriptionTypeText,
|
||||||
|
erpSyncText,
|
||||||
|
mapOrderDetailDisplay,
|
||||||
|
mapOrderProductItem,
|
||||||
|
} from '@/subPackages/sub_business_shared/common/display.js'
|
||||||
|
import { formatMoney } from '@/subPackages/sub_business_shared/common/format.js'
|
||||||
|
|
||||||
|
import businessMixin from '@/subPackages/sub_business_shared/common/businessMixin.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [businessMixin],
|
||||||
|
components: { BusinessPageLayout, 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 false
|
||||||
|
// if (!this.bizCap.canRefund) return false
|
||||||
|
// return Number(this.info.is_pay) === 1
|
||||||
|
},
|
||||||
|
canShip() {
|
||||||
|
return false
|
||||||
|
// return this.bizCap.canShip && Number(this.info.status) === 1 && Number(this.info.is_pay) === 1
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onLoad(q) {
|
||||||
|
this.id = Number(q.id || 0)
|
||||||
|
this.load()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
load() {
|
||||||
|
this.bizApi.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() {
|
||||||
|
this.bizApi.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
|
||||||
|
this.bizApi.refundOrder({ id: this.id, refund_reason: '管理员退款' }).then(w => {
|
||||||
|
if (w.ok) {
|
||||||
|
uni.showToast({ title: '已提交' })
|
||||||
|
this.load()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
ship() {
|
||||||
|
uni.showModal({
|
||||||
|
title: '确认发货',
|
||||||
|
content: '确定将该订单标记为已发货?',
|
||||||
|
success: (r) => {
|
||||||
|
if (!r.confirm) return
|
||||||
|
this.bizApi.wareSendOrder({ id: this.id, cancel_remark: '' }).then(w => {
|
||||||
|
if (w.ok) {
|
||||||
|
uni.showToast({ title: '发货成功' })
|
||||||
|
this.load()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
openPrescription() {
|
||||||
|
const pid = this.detailExtra.prescriptionId
|
||||||
|
if (!pid) return
|
||||||
|
this.$refs.prescriptionPopup.open(pid)
|
||||||
|
},
|
||||||
|
goTrace() {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `${this.bizRoot}/order/trace/index?order_id=${this.id}&scene=product`,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</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-outline {
|
||||||
|
background: #fff;
|
||||||
|
color: $theme-primary;
|
||||||
|
border: 1rpx solid $theme-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 次要操作(退款)给固定宽度 */
|
||||||
|
.btn-warn {
|
||||||
|
width: 220rpx;
|
||||||
|
background: #fff;
|
||||||
|
color: $color-danger;
|
||||||
|
border: 2rpx solid $color-danger;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
583
subPackages/sub_business_shared/order/index.vue
Normal file
583
subPackages/sub_business_shared/order/index.vue
Normal file
@@ -0,0 +1,583 @@
|
|||||||
|
<template>
|
||||||
|
<business-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" />
|
||||||
|
</business-page-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import BusinessPageLayout from '@/subPackages/sub_business_shared/components/business-page-layout.vue'
|
||||||
|
import PageStickyHeader from '@/subPackages/sub_business_shared/components/PageStickyHeader.vue'
|
||||||
|
import CollapsibleFilterPanel from '@/subPackages/sub_business_shared/components/CollapsibleFilterPanel.vue'
|
||||||
|
import PrescriptionDetailPopup from './components/PrescriptionDetailPopup.vue'
|
||||||
|
import { withWxKey } from '@/utils/wxListKey.js'
|
||||||
|
import { mapOrderListRow } from '@/subPackages/sub_business_shared/common/display.js'
|
||||||
|
import { formatMoney } from '@/subPackages/sub_business_shared/common/format.js'
|
||||||
|
import {
|
||||||
|
buildOrderListParams,
|
||||||
|
buildOrderSaleAmountParams,
|
||||||
|
} from '@/subPackages/sub_business_shared/common/orderParams.js'
|
||||||
|
import {
|
||||||
|
loadOrderFilter,
|
||||||
|
saveOrderFilter,
|
||||||
|
clearOrderFilter,
|
||||||
|
defaultOrderFilter,
|
||||||
|
} from '@/subPackages/sub_business_shared/common/filterCache.js'
|
||||||
|
import businessMixin from '@/subPackages/sub_business_shared/common/businessMixin.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 {
|
||||||
|
mixins: [businessMixin],
|
||||||
|
components: { BusinessPageLayout, 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())
|
||||||
|
this.bizApi.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())
|
||||||
|
this.bizApi.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(),
|
||||||
|
})
|
||||||
|
this.bizApi.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: `${this.bizRoot}/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>
|
||||||
480
subPackages/sub_business_shared/order/trace/index.vue
Normal file
480
subPackages/sub_business_shared/order/trace/index.vue
Normal file
@@ -0,0 +1,480 @@
|
|||||||
|
<template>
|
||||||
|
<business-page-layout title="订单追溯" :show-back="true">
|
||||||
|
<view class="trace-container">
|
||||||
|
<view v-if="loading" class="loading-tip">加载中...</view>
|
||||||
|
|
||||||
|
<template v-else-if="traceData">
|
||||||
|
<!-- 摘要 -->
|
||||||
|
<view class="summary-card">
|
||||||
|
<view class="row"><text class="label">商品订单</text><text class="value">{{ summary.product_order_no || '—' }}</text></view>
|
||||||
|
<view class="row" v-if="summary.register_order_no"><text class="label">挂号订单</text><text class="value">{{ summary.register_order_no }}</text></view>
|
||||||
|
<view class="row" v-if="summary.prescription_no"><text class="label">处方号</text><text class="value">{{ summary.prescription_no }}</text></view>
|
||||||
|
<view class="row"><text class="label">结算状态</text><text class="value">{{ summary.is_settled === 1 ? '已结算' : '未结算' }}</text></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 主 Tab -->
|
||||||
|
<view class="main-tabs">
|
||||||
|
<view
|
||||||
|
v-for="tab in mainTabs"
|
||||||
|
:key="tab.key"
|
||||||
|
class="main-tab"
|
||||||
|
:class="{ active: activeTab === tab.key }"
|
||||||
|
@click="switchMainTab(tab.key)"
|
||||||
|
>{{ tab.label }}</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 时间线 -->
|
||||||
|
<view v-show="activeTab === 'timeline'" class="tab-panel">
|
||||||
|
<view v-if="!timeline.length" class="empty">暂无时间线数据</view>
|
||||||
|
<view v-for="(node, idx) in timeline" :key="node.key || idx" class="timeline-item">
|
||||||
|
<view class="tl-dot" />
|
||||||
|
<view class="tl-body">
|
||||||
|
<view class="tl-title">{{ node.title }}</view>
|
||||||
|
<view class="tl-meta">
|
||||||
|
<text v-if="node.time">{{ node.time }}</text>
|
||||||
|
<text v-if="node.status" class="tl-status">{{ node.status }}</text>
|
||||||
|
</view>
|
||||||
|
<view v-if="node.extra" class="tl-extra">{{ node.extra }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 分账明细 -->
|
||||||
|
<view v-show="activeTab === 'ledger'" class="tab-panel">
|
||||||
|
<view v-if="ledgerLoading" class="loading-tip">加载分账...</view>
|
||||||
|
<template v-else>
|
||||||
|
<view v-if="hasRegisterLedger" class="sub-tabs">
|
||||||
|
<view
|
||||||
|
class="sub-tab"
|
||||||
|
:class="{ active: ledgerSubTab === 'product' }"
|
||||||
|
@click="switchLedgerSub('product')"
|
||||||
|
>商品订单</view>
|
||||||
|
<view
|
||||||
|
class="sub-tab"
|
||||||
|
:class="{ active: ledgerSubTab === 'register' }"
|
||||||
|
@click="switchLedgerSub('register')"
|
||||||
|
>挂号订单</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="currentLedgerItems.length">
|
||||||
|
<view v-for="row in currentLedgerItems" :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" v-if="row.drug_name"><text class="label">药品</text><text class="value">{{ row.drug_name }}</text></view>
|
||||||
|
<view class="row" v-if="row.specification"><text class="label">规格</text><text class="value">{{ row.specification }}</text></view>
|
||||||
|
<view class="row"><text class="label">分账金额</text><text class="value amount">{{ formatMoney(row.money) }}</text></view>
|
||||||
|
<view class="row"><text class="label">结算状态</text><text class="value">{{ row.status_txt || '-' }}</text></view>
|
||||||
|
<view class="row" v-if="row.created_at"><text class="label">时间</text><text class="value">{{ row.created_at }}</text></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-else class="empty">暂无分账明细</view>
|
||||||
|
</template>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 对账明细 -->
|
||||||
|
<view v-show="activeTab === 'reconciliation'" class="tab-panel">
|
||||||
|
<view v-if="reconciliationLoading" class="loading-tip">加载对账...</view>
|
||||||
|
<template v-else>
|
||||||
|
<view v-if="reconciliationHint" class="hint-box">{{ reconciliationHint }}</view>
|
||||||
|
<template v-if="reconciliationSummary">
|
||||||
|
<view class="recon-summary-card">
|
||||||
|
<view class="recon-head">
|
||||||
|
<text class="recon-title">对账汇总</text>
|
||||||
|
<text v-if="reconciliationSummary.registrationPrice !== '¥0.00'" class="mini-tag">
|
||||||
|
挂号费 {{ reconciliationSummary.registrationPrice }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
<view v-if="reconciliationSummary.registerOrderNo" class="recon-sub">
|
||||||
|
挂号单号 {{ reconciliationSummary.registerOrderNo }}
|
||||||
|
</view>
|
||||||
|
<view class="hero-data">
|
||||||
|
<view class="data-box">
|
||||||
|
<text class="data-label">总销售额</text>
|
||||||
|
<text class="data-val text-primary">{{ reconciliationSummary.totalSales }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="data-divider" />
|
||||||
|
<view class="data-box">
|
||||||
|
<text class="data-label">总供货额</text>
|
||||||
|
<text class="data-val">{{ reconciliationSummary.totalSupply }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="dashboard-grid">
|
||||||
|
<view class="dashboard-island">
|
||||||
|
<view class="island-head">
|
||||||
|
<text class="island-title">草药</text>
|
||||||
|
<text class="island-sales">销量 {{ reconciliationSummary.herbal.sales }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="island-line"><text>销售</text><text>{{ reconciliationSummary.herbal.salesPrice }}</text></view>
|
||||||
|
<view class="island-line"><text>供货</text><text>{{ reconciliationSummary.herbal.supplyPrice }}</text></view>
|
||||||
|
</view>
|
||||||
|
<view class="dashboard-island">
|
||||||
|
<view class="island-head">
|
||||||
|
<text class="island-title">西药</text>
|
||||||
|
<text class="island-sales">销量 {{ reconciliationSummary.medicine.sales }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="island-line"><text>销售</text><text>{{ reconciliationSummary.medicine.salesPrice }}</text></view>
|
||||||
|
<view class="island-line"><text>供货</text><text>{{ reconciliationSummary.medicine.supplyPrice }}</text></view>
|
||||||
|
</view>
|
||||||
|
<view class="dashboard-island">
|
||||||
|
<view class="island-head">
|
||||||
|
<text class="island-title">服务包</text>
|
||||||
|
<text class="island-sales">销量 {{ reconciliationSummary.servicePackage.sales }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="island-line"><text>销售</text><text>{{ reconciliationSummary.servicePackage.salesPrice }}</text></view>
|
||||||
|
<view class="island-line"><text>供货</text><text>{{ reconciliationSummary.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>{{ reconciliationSummary.otherFees.treatment }}</text></view>
|
||||||
|
<view class="island-line"><text>加工费</text><text>{{ reconciliationSummary.otherFees.process }}</text></view>
|
||||||
|
<view class="island-line"><text>快递费</text><text>{{ reconciliationSummary.otherFees.express }}</text></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
<view v-if="reconciliationItems.length" class="recon-drug-section">
|
||||||
|
<view class="section-title">药品明细</view>
|
||||||
|
<view v-for="row in reconciliationItems" :key="row._wxKey" class="detail-card">
|
||||||
|
<view class="row"><text class="label">药品</text><text class="value">{{ row.drug_name || row.drug_id || '-' }}</text></view>
|
||||||
|
<view class="row"><text class="label">数量</text><text class="value">{{ row.number }}</text></view>
|
||||||
|
<view class="row"><text class="label">销售价</text><text class="value">{{ formatMoney(row.total_price) }}</text></view>
|
||||||
|
<view class="row"><text class="label">供货价</text><text class="value">{{ formatMoney(row.total_buy_price) }}</text></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-else-if="!reconciliationHint && !reconciliationSummary" class="empty">暂无对账明细</view>
|
||||||
|
</template>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<view v-else-if="!loading" class="empty">加载失败,请返回重试</view>
|
||||||
|
</view>
|
||||||
|
</business-page-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import BusinessPageLayout from '@/subPackages/sub_business_shared/components/business-page-layout.vue'
|
||||||
|
import { formatMoney } from '@/subPackages/sub_business_shared/common/format.js'
|
||||||
|
import { mapReconciliationOrderSummary } from '@/subPackages/sub_business_shared/common/display.js'
|
||||||
|
import { withWxKey } from '@/utils/wxListKey.js'
|
||||||
|
|
||||||
|
import businessMixin from '@/subPackages/sub_business_shared/common/businessMixin.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [businessMixin],
|
||||||
|
components: { BusinessPageLayout },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
orderId: 0,
|
||||||
|
scene: 'product',
|
||||||
|
loading: true,
|
||||||
|
traceData: null,
|
||||||
|
activeTab: 'timeline',
|
||||||
|
ledgerSubTab: 'product',
|
||||||
|
ledgerProduct: null,
|
||||||
|
ledgerRegister: null,
|
||||||
|
ledgerLoading: false,
|
||||||
|
reconciliationData: null,
|
||||||
|
reconciliationLoading: false,
|
||||||
|
loadedTabs: {},
|
||||||
|
mainTabs: [
|
||||||
|
{ key: 'timeline', label: '时间线' },
|
||||||
|
{ key: 'ledger', label: '分账' },
|
||||||
|
{ key: 'reconciliation', label: '对账' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
summary() {
|
||||||
|
return this.traceData?.summary || {}
|
||||||
|
},
|
||||||
|
timeline() {
|
||||||
|
return this.traceData?.timeline || []
|
||||||
|
},
|
||||||
|
links() {
|
||||||
|
return this.traceData?.links || {}
|
||||||
|
},
|
||||||
|
hasRegisterLedger() {
|
||||||
|
return !!(this.links.register_id && this.ledgerRegister !== null)
|
||||||
|
},
|
||||||
|
currentLedgerItems() {
|
||||||
|
const data = this.ledgerSubTab === 'register' ? this.ledgerRegister : this.ledgerProduct
|
||||||
|
const items = data?.items || []
|
||||||
|
return withWxKey(items, 'id', 'traceLed')
|
||||||
|
},
|
||||||
|
reconciliationItems() {
|
||||||
|
const items = this.reconciliationData?.items || []
|
||||||
|
return withWxKey(items, 'id', 'traceRec')
|
||||||
|
},
|
||||||
|
reconciliationSummary() {
|
||||||
|
return mapReconciliationOrderSummary(this.reconciliationData?.summary)
|
||||||
|
},
|
||||||
|
reconciliationHint() {
|
||||||
|
return this.reconciliationData?.reconciliation_hint || this.summary.reconciliation_hint || ''
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onLoad(q) {
|
||||||
|
this.orderId = Number(q.order_id || 0)
|
||||||
|
this.scene = q.scene || 'product'
|
||||||
|
this.loadTrace()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
formatMoney,
|
||||||
|
loadTrace() {
|
||||||
|
if (this.orderId <= 0) {
|
||||||
|
this.loading = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.loading = true
|
||||||
|
this.bizApi.getOrderTrace({ scene: this.scene, order_id: this.orderId }).then(w => {
|
||||||
|
if (w.ok) {
|
||||||
|
this.traceData = w.data
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: (w.res && w.res.message) || '加载失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
}).finally(() => { this.loading = false })
|
||||||
|
},
|
||||||
|
switchMainTab(key) {
|
||||||
|
this.activeTab = key
|
||||||
|
if (key === 'ledger' && !this.loadedTabs.ledger) {
|
||||||
|
this.loadLedger()
|
||||||
|
} else if (key === 'reconciliation' && !this.loadedTabs.reconciliation) {
|
||||||
|
this.loadReconciliation()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
switchLedgerSub(key) {
|
||||||
|
this.ledgerSubTab = key
|
||||||
|
},
|
||||||
|
loadLedger() {
|
||||||
|
const productId = this.links.product_order_id
|
||||||
|
const registerId = this.links.register_id
|
||||||
|
if (!productId && !registerId) {
|
||||||
|
this.loadedTabs.ledger = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.ledgerLoading = true
|
||||||
|
const reqs = []
|
||||||
|
if (productId) {
|
||||||
|
reqs.push(
|
||||||
|
this.bizApi.getOrderLedgerDetail({ order_id: productId, order_type: 1, scope: 'store' }).then(w => {
|
||||||
|
this.ledgerProduct = w.ok ? w.data : null
|
||||||
|
})
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this.ledgerProduct = null
|
||||||
|
}
|
||||||
|
if (registerId) {
|
||||||
|
reqs.push(
|
||||||
|
this.bizApi.getOrderLedgerDetail({ order_id: registerId, order_type: 2, scope: 'store' }).then(w => {
|
||||||
|
this.ledgerRegister = w.ok ? w.data : null
|
||||||
|
})
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this.ledgerRegister = null
|
||||||
|
}
|
||||||
|
Promise.all(reqs).finally(() => {
|
||||||
|
this.ledgerLoading = false
|
||||||
|
this.loadedTabs.ledger = true
|
||||||
|
})
|
||||||
|
},
|
||||||
|
loadReconciliation() {
|
||||||
|
const productId = this.links.product_order_id || (this.scene === 'product' ? this.orderId : 0)
|
||||||
|
if (!productId) {
|
||||||
|
this.reconciliationData = { items: [], reconciliation_hint: '暂无关联商品订单' }
|
||||||
|
this.loadedTabs.reconciliation = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.reconciliationLoading = true
|
||||||
|
this.bizApi.getOrderReconciliationDetail({ product_order_id: productId }).then(w => {
|
||||||
|
if (w.ok) {
|
||||||
|
this.reconciliationData = w.data
|
||||||
|
}
|
||||||
|
}).finally(() => {
|
||||||
|
this.reconciliationLoading = false
|
||||||
|
this.loadedTabs.reconciliation = true
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.trace-container {
|
||||||
|
padding: 24rpx;
|
||||||
|
padding-bottom: 48rpx;
|
||||||
|
}
|
||||||
|
.loading-tip, .empty {
|
||||||
|
text-align: center;
|
||||||
|
color: #999;
|
||||||
|
font-size: 28rpx;
|
||||||
|
padding: 48rpx 0;
|
||||||
|
}
|
||||||
|
.summary-card, .detail-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 24rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
.summary-card .row, .detail-card .row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 26rpx;
|
||||||
|
padding: 8rpx 0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.label { color: #888; flex-shrink: 0; margin-right: 16rpx; }
|
||||||
|
.value { text-align: right; word-break: break-all; }
|
||||||
|
.amount { color: #1677ff; font-weight: 600; }
|
||||||
|
.main-tabs, .sub-tabs {
|
||||||
|
display: flex;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.main-tab, .sub-tab {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
padding: 20rpx 0;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.main-tab.active, .sub-tab.active {
|
||||||
|
color: #1677ff;
|
||||||
|
font-weight: 600;
|
||||||
|
background: #e6f4ff;
|
||||||
|
}
|
||||||
|
.tab-panel { min-height: 200rpx; }
|
||||||
|
.timeline-item {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
padding-left: 8rpx;
|
||||||
|
}
|
||||||
|
.tl-dot {
|
||||||
|
width: 16rpx;
|
||||||
|
height: 16rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #1677ff;
|
||||||
|
margin-top: 10rpx;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.tl-body { flex: 1; background: #fff; border-radius: 12rpx; padding: 20rpx; }
|
||||||
|
.tl-title { font-size: 28rpx; font-weight: 600; color: #333; }
|
||||||
|
.tl-meta { font-size: 24rpx; color: #999; margin-top: 8rpx; }
|
||||||
|
.tl-status { margin-left: 12rpx; color: #1677ff; }
|
||||||
|
.tl-extra { font-size: 24rpx; color: #666; margin-top: 8rpx; }
|
||||||
|
.hint-box {
|
||||||
|
background: #fff7e6;
|
||||||
|
color: #d48806;
|
||||||
|
font-size: 26rpx;
|
||||||
|
padding: 20rpx 24rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
.sum-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
padding: 0 8rpx;
|
||||||
|
}
|
||||||
|
.recon-summary-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 24rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
.recon-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
.recon-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.recon-sub {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #888;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
.mini-tag {
|
||||||
|
font-size: 20rpx;
|
||||||
|
padding: 4rpx 12rpx;
|
||||||
|
background: rgba(106, 205, 187, 0.1);
|
||||||
|
color: #6ACDBB;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
}
|
||||||
|
.hero-data {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: #F8FBFB;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 24rpx 0;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
.data-box {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.data-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #888;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
.data-val {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.text-primary { color: #1677ff; }
|
||||||
|
.data-divider {
|
||||||
|
width: 1rpx;
|
||||||
|
height: 60rpx;
|
||||||
|
background: #eee;
|
||||||
|
}
|
||||||
|
.dashboard-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
.dashboard-island {
|
||||||
|
background: #F8FBFB;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 16rpx;
|
||||||
|
}
|
||||||
|
.island-head {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
.island-title {
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.island-sales {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
.island-line {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #666;
|
||||||
|
padding: 4rpx 0;
|
||||||
|
}
|
||||||
|
.recon-drug-section { margin-top: 8rpx; }
|
||||||
|
.section-title {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
padding: 0 8rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
802
subPackages/sub_business_shared/reconciliation/index.vue
Normal file
802
subPackages/sub_business_shared/reconciliation/index.vue
Normal file
@@ -0,0 +1,802 @@
|
|||||||
|
<template>
|
||||||
|
<business-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>
|
||||||
|
</business-page-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import BusinessPageLayout from '@/subPackages/sub_business_shared/components/business-page-layout.vue'
|
||||||
|
import PageStickyHeader from '@/subPackages/sub_business_shared/components/PageStickyHeader.vue'
|
||||||
|
import CollapsibleFilterPanel from '@/subPackages/sub_business_shared/components/CollapsibleFilterPanel.vue'
|
||||||
|
import { withWxKey } from '@/utils/wxListKey.js'
|
||||||
|
import {
|
||||||
|
mapReconciliationStoreRow,
|
||||||
|
mapReconciliationDrugRow,
|
||||||
|
mapReconciliationStoreCount,
|
||||||
|
mapReconciliationDrugCount,
|
||||||
|
} from '@/subPackages/sub_business_shared/common/display.js'
|
||||||
|
import {
|
||||||
|
buildStoreListParams,
|
||||||
|
buildDrugListParams,
|
||||||
|
buildSearchTimeQueryParams,
|
||||||
|
normalizeReconciliationResponse,
|
||||||
|
} from '@/subPackages/sub_business_shared/common/reconciliationParams.js'
|
||||||
|
import {
|
||||||
|
loadReconciliationFilter,
|
||||||
|
saveReconciliationFilter,
|
||||||
|
clearReconciliationFilter,
|
||||||
|
defaultReconciliationFilter,
|
||||||
|
} from '@/subPackages/sub_business_shared/common/filterCache.js'
|
||||||
|
|
||||||
|
import businessMixin from '@/subPackages/sub_business_shared/common/businessMixin.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [businessMixin],
|
||||||
|
components: { BusinessPageLayout, 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 apiFn = isDrug
|
||||||
|
? this.bizApi.getReconciliationDrugList
|
||||||
|
: this.bizApi.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,
|
||||||
|
})
|
||||||
|
apiFn.call(this.bizApi, 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')
|
||||||
|
}).catch(() => {
|
||||||
|
uni.showToast({ title: '查询失败', icon: 'none' })
|
||||||
|
this.list = []
|
||||||
|
this.statItems = []
|
||||||
|
}).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
|
||||||
|
}
|
||||||
|
this.bizApi.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
|
||||||
|
}
|
||||||
|
this.bizApi.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,133 @@
|
|||||||
|
<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>
|
||||||
|
import businessMixin from '@/subPackages/sub_business_shared/common/businessMixin.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [businessMixin],
|
||||||
|
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>
|
||||||
314
subPackages/sub_business_shared/warehouse/detail.vue
Normal file
314
subPackages/sub_business_shared/warehouse/detail.vue
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
<template>
|
||||||
|
<business-page-layout title="药品详情" :show-back="true">
|
||||||
|
<view v-if="info.id" class="page-container">
|
||||||
|
<!-- 头部信息卡片 -->
|
||||||
|
<view class="card hero-card">
|
||||||
|
<view class="image-wrapper">
|
||||||
|
<image class="hero" :src="display.imageUrl" mode="aspectFill" />
|
||||||
|
</view>
|
||||||
|
<view class="main-info">
|
||||||
|
<text class="drug-name">{{ display.drugName }}</text>
|
||||||
|
<text class="pinyin">拼音: {{ display.pinyin_simple || '--' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 核心数据卡片 -->
|
||||||
|
<view class="card data-card">
|
||||||
|
<view class="row">
|
||||||
|
<text class="label">状态</text>
|
||||||
|
<view class="status-tag" :class="display.isOnShelf ? 'tag-on' : 'tag-off'">
|
||||||
|
{{ display.statusText }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="row">
|
||||||
|
<text class="label">库存</text>
|
||||||
|
<text class="value stock-value">{{ display.stock }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="divider"></view>
|
||||||
|
|
||||||
|
<view class="row">
|
||||||
|
<text class="label">供货价</text>
|
||||||
|
<text class="value">{{ display.buyPrice }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="row">
|
||||||
|
<text class="label">建议售价</text>
|
||||||
|
<text class="value">{{ display.suggestPrice }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="row highlight-row">
|
||||||
|
<text class="label">当前售价</text>
|
||||||
|
<text class="value price-large">{{ display.price }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 底部操作按钮组 -->
|
||||||
|
<view v-if="info.id" class="action-group">
|
||||||
|
<view class="btn-large btn-outline" hover-class="btn-hover-opacity" @click="openPriceEdit">
|
||||||
|
修改售价
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="btn-large"
|
||||||
|
:class="display.isOnShelf ? 'btn-danger' : 'btn-primary'"
|
||||||
|
hover-class="btn-hover-opacity"
|
||||||
|
@click="toggleStatus"
|
||||||
|
>
|
||||||
|
{{ display.isOnShelf ? '下架该药品' : '上架该药品' }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<price-edit-modal
|
||||||
|
:visible="priceModalVisible"
|
||||||
|
:item="info.id ? info : null"
|
||||||
|
@close="closePriceEdit"
|
||||||
|
@confirm="onPriceConfirm"
|
||||||
|
/>
|
||||||
|
</business-page-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import BusinessPageLayout from '@/subPackages/sub_business_shared/components/business-page-layout.vue'
|
||||||
|
import PriceEditModal from './components/PriceEditModal.vue'
|
||||||
|
import { mapWarehouseRow } from '@/subPackages/sub_business_shared/common/display.js'
|
||||||
|
|
||||||
|
import businessMixin from '@/subPackages/sub_business_shared/common/businessMixin.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [businessMixin],
|
||||||
|
components: { BusinessPageLayout, 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() {
|
||||||
|
this.bizApi.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
|
||||||
|
this.bizApi.updateWarehousePrice({ id: this.id, price }).then(w => {
|
||||||
|
if (w.ok) {
|
||||||
|
uni.showToast({ title: '修改成功' })
|
||||||
|
this.closePriceEdit()
|
||||||
|
this.load()
|
||||||
|
}
|
||||||
|
}).finally(() => { this.priceSubmitting = false })
|
||||||
|
},
|
||||||
|
toggleStatus() {
|
||||||
|
this.bizApi.updateWarehouseStatus(this.id).then(w => {
|
||||||
|
if (w.ok) {
|
||||||
|
uni.showToast({ title: '操作成功' })
|
||||||
|
this.load()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
/* 同步了列表页的核心色板 */
|
||||||
|
$theme-color: #6acdbb;
|
||||||
|
$theme-color-light: rgba(106, 205, 187, 0.12);
|
||||||
|
$text-primary: #2c3e50;
|
||||||
|
$text-secondary: #8a97a3;
|
||||||
|
$text-light: #b0b8c1;
|
||||||
|
$bg-color: #f7f9fa;
|
||||||
|
$border-color: #ebedf0;
|
||||||
|
$danger-color: #ff6b6b;
|
||||||
|
$danger-color-light: rgba(255, 107, 107, 0.1);
|
||||||
|
|
||||||
|
.page-container {
|
||||||
|
padding: 24rpx;
|
||||||
|
padding-bottom: 60rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 32rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 头部卡片特异样式 */
|
||||||
|
.hero-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
background: $bg-color;
|
||||||
|
border: 1rpx solid $border-color;
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
width: 100%;
|
||||||
|
height: 400rpx;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-info {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drug-name {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $text-primary;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pinyin {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: $text-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 核心数据卡片样式 */
|
||||||
|
.data-card {
|
||||||
|
padding: 16rpx 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 24rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
height: 1rpx;
|
||||||
|
background-color: $border-color;
|
||||||
|
margin: 8rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: $text-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: $text-primary;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-value {
|
||||||
|
font-family: monospace; /* 数字更清晰 */
|
||||||
|
font-size: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight-row {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
background: #fafbfa; /* 极浅的背景强调 */
|
||||||
|
margin: 0 -32rpx;
|
||||||
|
padding: 32rpx;
|
||||||
|
border-bottom-left-radius: 24rpx;
|
||||||
|
border-bottom-right-radius: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight-row .label {
|
||||||
|
color: $text-primary;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-large {
|
||||||
|
font-size: 40rpx;
|
||||||
|
color: $theme-color;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 标签样式 */
|
||||||
|
.status-tag {
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 6rpx 20rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-on {
|
||||||
|
color: $theme-color;
|
||||||
|
background: $theme-color-light;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-off {
|
||||||
|
color: $text-secondary;
|
||||||
|
background: $bg-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 底部操作按钮组 */
|
||||||
|
.action-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24rpx;
|
||||||
|
margin-top: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-large {
|
||||||
|
width: 100%;
|
||||||
|
height: 96rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 48rpx;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: $theme-color;
|
||||||
|
color: #ffffff;
|
||||||
|
box-shadow: 0 8rpx 24rpx $theme-color-light;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: $danger-color;
|
||||||
|
color: #ffffff;
|
||||||
|
box-shadow: 0 8rpx 24rpx $danger-color-light;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline {
|
||||||
|
background: #ffffff;
|
||||||
|
color: $theme-color;
|
||||||
|
border: 2rpx solid $theme-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-hover-opacity {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
381
subPackages/sub_business_shared/warehouse/index.vue
Normal file
381
subPackages/sub_business_shared/warehouse/index.vue
Normal file
@@ -0,0 +1,381 @@
|
|||||||
|
<template>
|
||||||
|
<business-page-layout :title="pageTitle" :show-back="true">
|
||||||
|
<!-- 搜索栏优化:胶囊式设计,增加视觉呼吸感 -->
|
||||||
|
<view class="search-wrapper">
|
||||||
|
<view class="search-bar">
|
||||||
|
<input class="search-input" v-model="keyword" placeholder="搜索药品名称" placeholder-class="search-placeholder" @confirm="reload" />
|
||||||
|
<view class="search-btn" hover-class="btn-hover" @click="reload">搜索</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="list-container">
|
||||||
|
<view
|
||||||
|
v-for="item in list"
|
||||||
|
:key="item._wxKey"
|
||||||
|
class="item-card"
|
||||||
|
hover-class="card-hover"
|
||||||
|
@click="goDetail(item)"
|
||||||
|
>
|
||||||
|
<!-- 左侧缩略图 -->
|
||||||
|
<image class="thumb" :src="rowDisplay(item).imageUrl" mode="aspectFill" />
|
||||||
|
|
||||||
|
<!-- 右侧内容区 -->
|
||||||
|
<view class="content">
|
||||||
|
<!-- 顶部:标题与状态标签对齐 -->
|
||||||
|
<view class="header-row">
|
||||||
|
<text class="title">{{ rowDisplay(item).drugName }}</text>
|
||||||
|
<view class="status-tag" :class="rowDisplay(item).isOnShelf ? 'tag-on' : 'tag-off'">
|
||||||
|
{{ rowDisplay(item).statusText }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 中部:详细信息 -->
|
||||||
|
<view class="info-group">
|
||||||
|
<text class="info-text">供货 {{ rowDisplay(item).buyPrice }} · 售价 {{ rowDisplay(item).price }}</text>
|
||||||
|
<text class="info-text">库存 {{ rowDisplay(item).stock }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 底部:操作按钮横向排列,增加点击区域与反馈 -->
|
||||||
|
<view class="action-row" @click.stop>
|
||||||
|
<view class="action-btn outline" hover-class="btn-hover-opacity" @click.stop="openPriceEdit(item)">
|
||||||
|
改价
|
||||||
|
</view>
|
||||||
|
<view class="action-btn" :class="rowDisplay(item).isOnShelf ? 'btn-danger' : 'btn-primary'" hover-class="btn-hover-opacity" @click.stop="toggleStatus(item)">
|
||||||
|
{{ rowDisplay(item).isOnShelf ? '下架' : '上架' }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 空状态优化 -->
|
||||||
|
<view v-if="!loading && !list.length" class="empty-state">
|
||||||
|
<view class="empty-icon"></view>
|
||||||
|
<text class="empty-text">暂无药品数据</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<price-edit-modal
|
||||||
|
:visible="priceModalVisible"
|
||||||
|
:item="priceEditItem"
|
||||||
|
@close="closePriceEdit"
|
||||||
|
@confirm="onPriceConfirm"
|
||||||
|
/>
|
||||||
|
</business-page-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import BusinessPageLayout from '@/subPackages/sub_business_shared/components/business-page-layout.vue'
|
||||||
|
import PriceEditModal from './components/PriceEditModal.vue'
|
||||||
|
import { withWxKey } from '@/utils/wxListKey.js'
|
||||||
|
import { mapWarehouseRow } from '@/subPackages/sub_business_shared/common/display.js'
|
||||||
|
|
||||||
|
import businessMixin from '@/subPackages/sub_business_shared/common/businessMixin.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [businessMixin],
|
||||||
|
components: { BusinessPageLayout, 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
|
||||||
|
}
|
||||||
|
this.bizApi.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: `${this.bizRoot}/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
|
||||||
|
this.bizApi.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
|
||||||
|
this.bizApi.updateWarehouseStatus(item.id).then(w => {
|
||||||
|
if (w.ok) {
|
||||||
|
uni.showToast({ title: '操作成功' })
|
||||||
|
item.status = item.status === 2 ? 1 : 2
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
/* 核心主题色变量提取,方便后续统一管理 */
|
||||||
|
$theme-color: #6acdbb;
|
||||||
|
$theme-color-light: rgba(106, 205, 187, 0.12);
|
||||||
|
$text-primary: #2c3e50;
|
||||||
|
$text-secondary: #8a97a3;
|
||||||
|
$text-light: #b0b8c1;
|
||||||
|
$bg-color: #f7f9fa;
|
||||||
|
$border-color: #ebedf0;
|
||||||
|
$danger-color: #ff6b6b;
|
||||||
|
$danger-color-light: rgba(255, 107, 107, 0.1);
|
||||||
|
|
||||||
|
.search-wrapper {
|
||||||
|
padding: 16rpx 24rpx;
|
||||||
|
background-color: transparent;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: #ffffff;
|
||||||
|
padding: 12rpx 16rpx 12rpx 32rpx;
|
||||||
|
border-radius: 40rpx;
|
||||||
|
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: $text-primary;
|
||||||
|
height: 60rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-placeholder {
|
||||||
|
color: $text-light;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-btn {
|
||||||
|
background: $theme-color;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 12rpx 36rpx;
|
||||||
|
border-radius: 30rpx;
|
||||||
|
margin-left: 16rpx;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-container {
|
||||||
|
padding: 0 24rpx 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-card {
|
||||||
|
display: flex;
|
||||||
|
background: #ffffff;
|
||||||
|
padding: 24rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.03);
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-hover {
|
||||||
|
transform: scale(0.99);
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb {
|
||||||
|
width: 140rpx;
|
||||||
|
height: 140rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: $bg-color;
|
||||||
|
border: 1rpx solid $border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 24rpx;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: $text-primary;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.4;
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 16rpx;
|
||||||
|
/* 单行截断 */
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag {
|
||||||
|
font-size: 20rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 4rpx 14rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-on {
|
||||||
|
color: $theme-color;
|
||||||
|
background: $theme-color-light;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-off {
|
||||||
|
color: $text-secondary;
|
||||||
|
background: $bg-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6rpx;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: $text-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
font-size: 24rpx;
|
||||||
|
padding: 10rpx 28rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.5;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn.outline {
|
||||||
|
color: $text-primary;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1rpx solid $border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn.btn-primary {
|
||||||
|
color: #ffffff;
|
||||||
|
background: $theme-color;
|
||||||
|
border: 1rpx solid $theme-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn.btn-danger {
|
||||||
|
color: $danger-color;
|
||||||
|
background: $danger-color-light;
|
||||||
|
border: 1rpx solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-hover-opacity {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 120rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
width: 200rpx;
|
||||||
|
height: 200rpx;
|
||||||
|
background-color: $bg-color;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 用伪元素做一个简单的空状态图形占位,有条件可替换为真实的Image/SVG */
|
||||||
|
.empty-icon::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 80rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
border: 8rpx solid $border-color;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
color: $text-light;
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
<template>
|
||||||
|
<business-page-layout title="资金变动" :show-back="true">
|
||||||
|
<view class="page-container">
|
||||||
|
<view class="filter-bar">
|
||||||
|
<picker :range="sourceTypeLabels" @change="onSourceTypeChange">
|
||||||
|
<view class="picker-value">{{ currentSourceLabel }}</view>
|
||||||
|
</picker>
|
||||||
|
<input
|
||||||
|
v-model="orderNo"
|
||||||
|
class="search-input"
|
||||||
|
placeholder="订单号"
|
||||||
|
confirm-type="search"
|
||||||
|
@confirm="reload"
|
||||||
|
/>
|
||||||
|
<view class="search-btn" @click="reload">搜索</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="list-wrap">
|
||||||
|
<view v-for="item in list" :key="item.id" class="record-card">
|
||||||
|
<view class="row head">
|
||||||
|
<text class="source">{{ item.source_type_txt || '-' }}</text>
|
||||||
|
<text class="change" :class="Number(item.change_amount) >= 0 ? 'inc' : 'dec'">
|
||||||
|
{{ item.change_amount }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
<view class="row"><text class="label">变更字段</text><text class="value">{{ item.field_name_txt || item.field_name }}</text></view>
|
||||||
|
<view class="row"><text class="label">变动前</text><text class="value">{{ item.before_amount }}</text></view>
|
||||||
|
<view class="row"><text class="label">变动后</text><text class="value">{{ item.after_amount }}</text></view>
|
||||||
|
<view class="row" v-if="item.order_no"><text class="label">订单号</text><text class="value">{{ item.order_no }}</text></view>
|
||||||
|
<view class="row"><text class="label">时间</text><text class="value">{{ item.created_at }}</text></view>
|
||||||
|
<view class="row" v-if="item.remark"><text class="label">备注</text><text class="value">{{ item.remark }}</text></view>
|
||||||
|
<view
|
||||||
|
v-if="Number(item.order_type) === 1 && item.order_id"
|
||||||
|
class="order-link"
|
||||||
|
@click="goOrderDetail(item)"
|
||||||
|
>查看订单详情</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="!loading && list.length === 0" class="empty-state">暂无资金变动记录</view>
|
||||||
|
</view>
|
||||||
|
</business-page-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import BusinessPageLayout from '@/subPackages/sub_business_shared/components/business-page-layout.vue'
|
||||||
|
import businessMixin from '@/subPackages/sub_business_shared/common/businessMixin.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [businessMixin],
|
||||||
|
components: { BusinessPageLayout },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
list: [],
|
||||||
|
page: 1,
|
||||||
|
loading: false,
|
||||||
|
orderNo: '',
|
||||||
|
sourceType: '',
|
||||||
|
sourceTypeOptions: [
|
||||||
|
{ label: '全部来源', value: '' },
|
||||||
|
{ label: '分账', value: 'settle' },
|
||||||
|
{ label: '提现', value: 'withdraw' },
|
||||||
|
{ label: '退款', value: 'refund' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
sourceTypeLabels() {
|
||||||
|
return this.sourceTypeOptions.map(o => o.label)
|
||||||
|
},
|
||||||
|
currentSourceLabel() {
|
||||||
|
const hit = this.sourceTypeOptions.find(o => o.value === this.sourceType)
|
||||||
|
return hit ? hit.label : '全部来源'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onShow() {
|
||||||
|
this.reload()
|
||||||
|
},
|
||||||
|
onReachBottom() {
|
||||||
|
this.load(this.page + 1, true)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onSourceTypeChange(e) {
|
||||||
|
const idx = Number(e.detail.value || 0)
|
||||||
|
this.sourceType = this.sourceTypeOptions[idx].value
|
||||||
|
this.reload()
|
||||||
|
},
|
||||||
|
reload() {
|
||||||
|
this.load(1, false)
|
||||||
|
},
|
||||||
|
load(page, append) {
|
||||||
|
this.loading = true
|
||||||
|
const params = { page, pageSize: 15 }
|
||||||
|
if (this.sourceType) params.source_type = this.sourceType
|
||||||
|
if (this.orderNo.trim()) params.order_no = this.orderNo.trim()
|
||||||
|
this.bizApi.getAccountChangeLogList(params).then(w => {
|
||||||
|
const items = (w.data && w.data.items) || []
|
||||||
|
this.list = append ? this.list.concat(items) : items
|
||||||
|
this.page = page
|
||||||
|
}).finally(() => { this.loading = false })
|
||||||
|
},
|
||||||
|
goOrderDetail(item) {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `${this.bizRoot}/order/detail?id=${item.order_id}`,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
$color-bg-base: #F5F7F8;
|
||||||
|
$color-text-main: #1D2129;
|
||||||
|
$color-text-muted: #86909C;
|
||||||
|
$theme-primary: #6acdbb;
|
||||||
|
|
||||||
|
.page-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: $color-bg-base;
|
||||||
|
padding: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 12rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-value,
|
||||||
|
.search-input {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 16rpx 20rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-value {
|
||||||
|
min-width: 160rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-btn {
|
||||||
|
background: $theme-primary;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 16rpx 24rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
padding: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 26rpx;
|
||||||
|
padding: 8rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row.head {
|
||||||
|
border-bottom: 1rpx dashed #E5E6EB;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
padding-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source {
|
||||||
|
font-weight: 600;
|
||||||
|
color: $color-text-main;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change {
|
||||||
|
font-weight: 700;
|
||||||
|
font-family: DIN Alternate, monospace, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inc { color: $theme-primary; }
|
||||||
|
.dec { color: #F53F3F; }
|
||||||
|
|
||||||
|
.label {
|
||||||
|
color: $color-text-muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
color: $color-text-main;
|
||||||
|
text-align: right;
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 16rpx;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-link {
|
||||||
|
margin-top: 16rpx;
|
||||||
|
text-align: right;
|
||||||
|
color: $theme-primary;
|
||||||
|
font-size: 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
color: $color-text-muted;
|
||||||
|
padding: 100rpx 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
105
subPackages/sub_business_shared/withdrawal/apply.vue
Normal file
105
subPackages/sub_business_shared/withdrawal/apply.vue
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
<template>
|
||||||
|
<business-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>
|
||||||
|
</business-page-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import BusinessPageLayout from '@/subPackages/sub_business_shared/components/business-page-layout.vue'
|
||||||
|
import { formatMoney } from '@/subPackages/sub_business_shared/common/format.js'
|
||||||
|
import businessMixin from '@/subPackages/sub_business_shared/common/businessMixin.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [businessMixin],
|
||||||
|
components: { BusinessPageLayout },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
amount: '',
|
||||||
|
balanceText: '-',
|
||||||
|
loading: false,
|
||||||
|
quickAmounts: [
|
||||||
|
{ key: '1000', value: 1000 },
|
||||||
|
{ key: '3000', value: 3000 },
|
||||||
|
{ key: '5000', value: 5000 },
|
||||||
|
{ key: '10000', value: 10000 },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLoad() {
|
||||||
|
if (!this.bizApi.getMyBalance) return
|
||||||
|
this.bizApi.getMyBalance().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
|
||||||
|
this.bizApi.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>
|
||||||
145
subPackages/sub_business_shared/withdrawal/card-edit.vue
Normal file
145
subPackages/sub_business_shared/withdrawal/card-edit.vue
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
<template>
|
||||||
|
<business-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>
|
||||||
|
</business-page-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import BusinessPageLayout from '@/subPackages/sub_business_shared/components/business-page-layout.vue'
|
||||||
|
import businessMixin from '@/subPackages/sub_business_shared/common/businessMixin.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [businessMixin],
|
||||||
|
components: { BusinessPageLayout },
|
||||||
|
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() {
|
||||||
|
this.bizApi.getMyCard && this.bizApi.getMyCard().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() {
|
||||||
|
if (!this.bizApi.saveMyCard) return
|
||||||
|
this.loading = true
|
||||||
|
this.bizApi.saveMyCard(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>
|
||||||
429
subPackages/sub_business_shared/withdrawal/index.vue
Normal file
429
subPackages/sub_business_shared/withdrawal/index.vue
Normal file
@@ -0,0 +1,429 @@
|
|||||||
|
<template>
|
||||||
|
<business-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>
|
||||||
|
</business-page-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import BusinessPageLayout from '@/subPackages/sub_business_shared/components/business-page-layout.vue'
|
||||||
|
import { withWxKey } from '@/utils/wxListKey.js'
|
||||||
|
import { formatMoney } from '@/subPackages/sub_business_shared/common/format.js'
|
||||||
|
import { withdrawalCheckStatusText, formatDateTime } from '@/subPackages/sub_business_shared/common/display.js'
|
||||||
|
|
||||||
|
import businessMixin from '@/subPackages/sub_business_shared/common/businessMixin.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [businessMixin],
|
||||||
|
components: { BusinessPageLayout },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
cardTitle: '我的账户',
|
||||||
|
card: {
|
||||||
|
bank_card: '',
|
||||||
|
bank_name: ''
|
||||||
|
},
|
||||||
|
balanceItems: [
|
||||||
|
{ key: 'balance', label: '可提现余额 (元)', display: '0.00' },
|
||||||
|
{ key: 'reviewing', label: '审核中金额 (元)', display: '0.00' },
|
||||||
|
{ key: 'pending', label: '待结算收益 (元)', display: '0.00' },
|
||||||
|
],
|
||||||
|
actions: [
|
||||||
|
{ key: 'apply', label: '申请提现' },
|
||||||
|
{ key: 'settlement', label: '结算记录' },
|
||||||
|
{ key: 'accountChange', 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: `${this.bizRoot}/withdrawal/apply` });
|
||||||
|
} else if (key === 'edit') {
|
||||||
|
uni.navigateTo({ url: `${this.bizRoot}/withdrawal/card-edit` });
|
||||||
|
} else if (key === 'settlement') {
|
||||||
|
uni.navigateTo({ url: `${this.bizRoot}/withdrawal/settlement/index` });
|
||||||
|
} else if (key === 'accountChange') {
|
||||||
|
uni.navigateTo({ url: `${this.bizRoot}/withdrawal/account-change/index` });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 提现详情跳转
|
||||||
|
goWithdrawalDetail(item) {
|
||||||
|
uni.navigateTo({ url: `${this.bizRoot}/withdrawal/record-detail?id=${item.id}` });
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取绑定的银行卡信息
|
||||||
|
loadCard() {
|
||||||
|
if (!this.bizApi.getMyCard) return
|
||||||
|
this.bizApi.getMyCard().then(w => {
|
||||||
|
if (w.ok && w.data) {
|
||||||
|
this.card = w.data;
|
||||||
|
this.cardTitle = w.data.bank_user_name || '我的账户';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取余额数据
|
||||||
|
loadBalance() {
|
||||||
|
if (!this.bizApi.getMyBalance) return
|
||||||
|
this.bizApi.getMyBalance().then(w => {
|
||||||
|
if (w.ok && w.data) {
|
||||||
|
const fmt = (v) => formatMoney(v || 0).replace('¥', '')
|
||||||
|
const map = {
|
||||||
|
balance: fmt(w.data.balance),
|
||||||
|
reviewing: fmt(w.data.withdrawn_frozen),
|
||||||
|
pending: fmt(w.data.pending_earnings),
|
||||||
|
}
|
||||||
|
this.balanceItems = this.balanceItems.map(item => ({
|
||||||
|
...item,
|
||||||
|
display: map[item.key] ?? '0.00',
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 刷新提现列表
|
||||||
|
reloadList() {
|
||||||
|
this.page = 1;
|
||||||
|
this.loadList(1, false);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 加载提现列表数据
|
||||||
|
loadList(page, append) {
|
||||||
|
this.loading = true;
|
||||||
|
this.bizApi.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(3, 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>
|
||||||
171
subPackages/sub_business_shared/withdrawal/record-detail.vue
Normal file
171
subPackages/sub_business_shared/withdrawal/record-detail.vue
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
<template>
|
||||||
|
<business-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>
|
||||||
|
</business-page-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import BusinessPageLayout from '@/subPackages/sub_business_shared/components/business-page-layout.vue'
|
||||||
|
import { formatMoney } from '@/subPackages/sub_business_shared/common/format.js'
|
||||||
|
import {
|
||||||
|
formatDateTime,
|
||||||
|
withdrawalCheckStatusText,
|
||||||
|
dakuanStatusText as dakuanStatusLabel,
|
||||||
|
} from '@/subPackages/sub_business_shared/common/display.js'
|
||||||
|
import businessMixin from '@/subPackages/sub_business_shared/common/businessMixin.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [businessMixin],
|
||||||
|
components: { BusinessPageLayout },
|
||||||
|
data() {
|
||||||
|
return { id: 0, info: {} }
|
||||||
|
},
|
||||||
|
onLoad(q) {
|
||||||
|
this.id = Number(q.id || 0)
|
||||||
|
this.bizApi.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>
|
||||||
203
subPackages/sub_business_shared/withdrawal/settlement/detail.vue
Normal file
203
subPackages/sub_business_shared/withdrawal/settlement/detail.vue
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
<template>
|
||||||
|
<business-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>
|
||||||
|
</business-page-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import BusinessPageLayout from '@/subPackages/sub_business_shared/components/business-page-layout.vue'
|
||||||
|
import { formatMoney } from '@/subPackages/sub_business_shared/common/format.js'
|
||||||
|
import { withWxKey } from '@/utils/wxListKey.js'
|
||||||
|
|
||||||
|
import businessMixin from '@/subPackages/sub_business_shared/common/businessMixin.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [businessMixin],
|
||||||
|
components: { BusinessPageLayout },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
ledgerLogId: 0,
|
||||||
|
orderId: 0,
|
||||||
|
orderType: 0,
|
||||||
|
detail: { items: [], sum_money: '0' },
|
||||||
|
ledgerLogSummary: null,
|
||||||
|
ledgerItems: [],
|
||||||
|
loading: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLoad(q) {
|
||||||
|
this.ledgerLogId = Number(q.ledger_log_id || 0)
|
||||||
|
this.orderId = Number(q.order_id || 0)
|
||||||
|
this.orderType = Number(q.order_type || 0)
|
||||||
|
this.load()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
formatMoney,
|
||||||
|
load() {
|
||||||
|
this.loading = true
|
||||||
|
const params = this.orderId > 0 && this.orderType > 0
|
||||||
|
? { order_id: this.orderId, order_type: this.orderType }
|
||||||
|
: { ledger_log_id: this.ledgerLogId }
|
||||||
|
this.bizApi.getLedgerDetail(params).then(w => {
|
||||||
|
if (w.ok) {
|
||||||
|
this.detail = w.data || {}
|
||||||
|
const log = this.detail.ledger_log
|
||||||
|
this.ledgerLogSummary = log || null
|
||||||
|
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>
|
||||||
302
subPackages/sub_business_shared/withdrawal/settlement/index.vue
Normal file
302
subPackages/sub_business_shared/withdrawal/settlement/index.vue
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
<template>
|
||||||
|
<business-page-layout title="结算记录" :show-back="true">
|
||||||
|
<view class="settlement-container">
|
||||||
|
<view class="filter-bar">
|
||||||
|
<input
|
||||||
|
v-model="orderNo"
|
||||||
|
class="search-input"
|
||||||
|
placeholder="搜索订单号"
|
||||||
|
confirm-type="search"
|
||||||
|
@confirm="reload"
|
||||||
|
/>
|
||||||
|
<view class="search-btn" @click="reload">搜索</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="merge-bar">
|
||||||
|
<text class="merge-label">同订单合并</text>
|
||||||
|
<switch :checked="mergeByOrder" color="#6acdbb" @change="onMergeChange" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="list-wrap">
|
||||||
|
<view
|
||||||
|
v-for="item in list"
|
||||||
|
:key="item.id"
|
||||||
|
class="premium-card"
|
||||||
|
>
|
||||||
|
<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" v-if="rowDisplay(item).orderTypeText">
|
||||||
|
<text class="label">订单类型</text>
|
||||||
|
<text class="value">{{ rowDisplay(item).orderTypeText }}</text>
|
||||||
|
</view>
|
||||||
|
<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 class="card-actions">
|
||||||
|
<text class="action-link" @click.stop="goSettlementDetail(item)">结算明细</text>
|
||||||
|
<text
|
||||||
|
v-if="Number(item.order_type) === 1"
|
||||||
|
class="action-link primary"
|
||||||
|
@click.stop="goOrderDetail(item)"
|
||||||
|
>订单详情</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="!loading && list.length === 0" class="empty-state">
|
||||||
|
暂无结算记录
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</business-page-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import BusinessPageLayout from '@/subPackages/sub_business_shared/components/business-page-layout.vue'
|
||||||
|
import { mapSettlementRow } from '@/subPackages/sub_business_shared/common/display.js'
|
||||||
|
import businessMixin from '@/subPackages/sub_business_shared/common/businessMixin.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [businessMixin],
|
||||||
|
components: { BusinessPageLayout },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
list: [],
|
||||||
|
page: 1,
|
||||||
|
loading: false,
|
||||||
|
mergeByOrder: true,
|
||||||
|
orderNo: '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onShow() {
|
||||||
|
this.reload()
|
||||||
|
},
|
||||||
|
onReachBottom() {
|
||||||
|
this.load(this.page + 1, true)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
rowDisplay(item) {
|
||||||
|
return mapSettlementRow(item)
|
||||||
|
},
|
||||||
|
onMergeChange(e) {
|
||||||
|
this.mergeByOrder = !!e.detail.value
|
||||||
|
this.reload()
|
||||||
|
},
|
||||||
|
reload() {
|
||||||
|
this.load(1, false)
|
||||||
|
},
|
||||||
|
load(page, append) {
|
||||||
|
this.loading = true
|
||||||
|
const params = {
|
||||||
|
page,
|
||||||
|
pageSize: 15,
|
||||||
|
merge_by_order: this.mergeByOrder ? 1 : 0,
|
||||||
|
exclude_zero_amount: 1,
|
||||||
|
}
|
||||||
|
if (this.orderNo.trim()) {
|
||||||
|
params.order_no = this.orderNo.trim()
|
||||||
|
}
|
||||||
|
this.bizApi.getLedgerLogList(params).then(w => {
|
||||||
|
const items = (w.data && w.data.items) || []
|
||||||
|
this.list = append ? this.list.concat(items) : items
|
||||||
|
this.page = page
|
||||||
|
}).finally(() => { this.loading = false })
|
||||||
|
},
|
||||||
|
goSettlementDetail(item) {
|
||||||
|
if (item.merged) {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `${this.bizRoot}/withdrawal/settlement/detail?order_id=${item.order_id}&order_type=${item.order_type}`,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `${this.bizRoot}/withdrawal/settlement/detail?ledger_log_id=${item.id}`,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
goOrderDetail(item) {
|
||||||
|
if (Number(item.order_type) !== 1 || !item.order_id) {
|
||||||
|
uni.showToast({ title: '暂不支持查看该订单详情', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `${this.bizRoot}/order/detail?id=${item.order_id}`,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
$color-bg-base: #F5F7F8;
|
||||||
|
$color-text-main: #1D2129;
|
||||||
|
$color-text-body: #4E5969;
|
||||||
|
$color-text-muted: #86909C;
|
||||||
|
$theme-primary: #6acdbb;
|
||||||
|
|
||||||
|
.settlement-container {
|
||||||
|
background-color: $color-bg-base;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
flex: 1;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 16rpx 24rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-btn {
|
||||||
|
background: $theme-primary;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 0 28rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merge-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 20rpx 28rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merge-label {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: $color-text-main;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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; }
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 32rpx;
|
||||||
|
margin-top: 24rpx;
|
||||||
|
padding-top: 20rpx;
|
||||||
|
border-top: 1rpx solid #F2F3F5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-link {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: $color-text-muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-link.primary {
|
||||||
|
color: $theme-primary;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
color: $color-text-muted;
|
||||||
|
padding: 120rpx 0;
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
export const TAB_WORKBENCH = 0
|
export const TAB_WORKBENCH = 0
|
||||||
export const TAB_MY = 1
|
export const TAB_MY = 1
|
||||||
|
const ROOT = '/subPackages/sub_clinic_salesperson';
|
||||||
export const CLINIC_TAB_ROUTES = [
|
export const CLINIC_TAB_ROUTES = [
|
||||||
'/subPackages/sub_clinic_admin/home/index',
|
'/subPackages/sub_clinic_admin/home/index',
|
||||||
'/subPackages/sub_clinic_admin/my/index',
|
'/subPackages/sub_clinic_admin/my/index',
|
||||||
@@ -9,14 +9,16 @@ export const CLINIC_TAB_ROUTES = [
|
|||||||
export function getClinicTabList() {
|
export function getClinicTabList() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
iconPath: require('@/static/image/w-sf.png'),
|
iconPath: 'home',
|
||||||
selectedIconPath: require('@/static/image/sf.png'),
|
selectedIconPath: 'home-fill',
|
||||||
text: '工作台',
|
text: '工作台',
|
||||||
|
pagePath: `${ROOT}/home/index`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
iconPath: require('@/static/image/w-wd.png'),
|
iconPath: 'account',
|
||||||
selectedIconPath: require('@/static/image/wd.png'),
|
selectedIconPath: 'account-fill',
|
||||||
text: '我的',
|
text: '我的',
|
||||||
|
pagePath: `${ROOT}/my/index`,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getClinicTabList, switchClinicTab } from '../common/tabbar.js'
|
import { getClinicTabList, switchClinicTab } from '@/subPackages/sub_clinic_admin/common/tabbar.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ClinicPageLayout',
|
name: 'ClinicPageLayout',
|
||||||
|
|||||||
@@ -2,44 +2,54 @@
|
|||||||
<clinic-page-layout title="工作台" :show-tabbar="true" :tab-index="0">
|
<clinic-page-layout title="工作台" :show-tabbar="true" :tab-index="0">
|
||||||
<view class="workspace-container">
|
<view class="workspace-container">
|
||||||
|
|
||||||
<!-- 头部品牌卡片:保留高质感的品牌渐变色,改为通栏无圆角 -->
|
<!-- 极简流线型顶部个人信息区(与推广员/平台管理风格一致) -->
|
||||||
<view class="brand-card">
|
<view class="hero-section">
|
||||||
<view class="brand-info">
|
<view class="brand-info-wrap">
|
||||||
<view class="badge">
|
<view class="brand-text">
|
||||||
<text class="badge-text">诊所管理系统</text>
|
<view class="badge"><text class="badge-text">诊所管理系统</text></view>
|
||||||
|
<text class="store-name">{{ storeName }}</text>
|
||||||
|
<text class="store-sub">欢迎回来,今天也要高效工作</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 诊所专属二维码 -->
|
||||||
|
<view class="qr-container" @click="openStoreQrCode">
|
||||||
|
<image
|
||||||
|
v-if="storeQrCode"
|
||||||
|
class="qr-image"
|
||||||
|
:src="storeQrCode"
|
||||||
|
mode="aspectFit"
|
||||||
|
/>
|
||||||
|
<view v-else class="qr-placeholder">
|
||||||
|
<u-icon name="scan" size="48" color="#666"></u-icon>
|
||||||
|
</view>
|
||||||
|
<text class="qr-hint">诊所名片</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="store-name">{{ storeName }}</text>
|
|
||||||
<text class="store-subtitle">欢迎回来,今天也要高效工作</text>
|
|
||||||
</view>
|
</view>
|
||||||
<!-- 极简几何装饰线,拒绝突兀圆块 -->
|
|
||||||
<view class="brand-decorator"></view>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 聚合面板分区 -->
|
<!-- 主体内容区:快捷服务(多彩微拟物双列卡片) -->
|
||||||
<view v-for="section in sections" :key="section.key" class="section-panel">
|
<view class="content-wrapper">
|
||||||
<!-- 面板标题 -->
|
<view v-for="section in sections" :key="section.key" class="section-panel">
|
||||||
<view class="panel-header">
|
<view class="panel-header">
|
||||||
<text class="panel-title">{{ section.title }}</text>
|
<text class="panel-title">{{ section.title }}</text>
|
||||||
<!-- 极简的装饰点阵 -->
|
|
||||||
<view class="panel-dots">
|
|
||||||
<text class="dot"></text><text class="dot"></text><text class="dot"></text>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 4列高密度网格 -->
|
<view class="service-grid">
|
||||||
<view class="grid-layout">
|
<view
|
||||||
<view
|
v-for="item in section.items"
|
||||||
v-for="item in section.items"
|
:key="item.key"
|
||||||
:key="item.key"
|
class="service-card"
|
||||||
class="grid-item"
|
hover-class="service-card-hover"
|
||||||
hover-class="grid-item-active"
|
@click="go(item.path)"
|
||||||
:hover-stay-time="100"
|
>
|
||||||
@click="go(item.path)"
|
<view class="service-icon-box" :style="{ background: item.theme.bg }">
|
||||||
>
|
<u-icon :name="item.icon" size="38" :color="item.theme.color"></u-icon>
|
||||||
<view class="icon-container">
|
</view>
|
||||||
<text class="icon-char">{{ item.icon }}</text>
|
<view class="service-info">
|
||||||
|
<text class="service-name">{{ item.label }}</text>
|
||||||
|
<text class="service-desc">{{ item.desc || '快捷进入' }}</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<text class="item-label">{{ item.label }}</text>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -49,22 +59,34 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ClinicPageLayout from '../components/clinic-page-layout.vue'
|
import ClinicPageLayout from '@/subPackages/sub_clinic_admin/components/clinic-page-layout.vue'
|
||||||
import { getWarehouseProductTypeOptions } from '@/api/clinicAdmin.js'
|
import { getWarehouseProductTypeOptions, getClinicAdminStoreQr } from '@/api/clinicAdmin.js'
|
||||||
|
import { openQrCodePreview, buildClinicQrPayload } from '@/utils/qrCodePreview.js'
|
||||||
|
|
||||||
|
// 全局主题色池
|
||||||
|
const GLOBAL_THEMES = [
|
||||||
|
{ bg: 'linear-gradient(135deg, #FFF7ED, #FFEDD5)', color: '#F97316' }, // 橙金
|
||||||
|
{ bg: 'linear-gradient(135deg, #EFF6FF, #DBEAFE)', color: '#3B82F6' }, // 科技蓝
|
||||||
|
{ bg: 'linear-gradient(135deg, #FAF5FF, #F3E8FF)', color: '#A855F7' }, // 优雅紫
|
||||||
|
{ bg: 'linear-gradient(135deg, #F0FDF4, #DCFCE7)', color: '#22C55E' }, // 安全绿
|
||||||
|
]
|
||||||
|
|
||||||
|
// 补充了 desc 字段以适配新的 UI
|
||||||
const FINANCE_MENUS = [
|
const FINANCE_MENUS = [
|
||||||
{ key: 'withdrawal', label: '提现管理', icon: '¥', path: '/subPackages/sub_clinic_admin/withdrawal/index' },
|
{ key: 'withdrawal', label: '提现管理', desc: '提现流水查询', icon: 'red-packet-fill', theme: GLOBAL_THEMES[0], path: '/subPackages/sub_clinic_admin/withdrawal/index' },
|
||||||
{ key: 'reconciliation', label: '对账单', icon: '账', path: '/subPackages/sub_clinic_admin/reconciliation/index' },
|
{ key: 'reconciliation', label: '对账单', desc: '财务对账明细', icon: 'file-text-fill', theme: GLOBAL_THEMES[1], path: '/subPackages/sub_clinic_admin/reconciliation/index' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const BUSINESS_MENUS = [
|
const BUSINESS_MENUS = [
|
||||||
{ key: 'order', label: '商品订单', icon: '单', path: '/subPackages/sub_clinic_admin/order/index' },
|
{ key: 'order', label: '商品订单', desc: '全平台订单概览', icon: 'bag-fill', theme: GLOBAL_THEMES[3], path: '/subPackages/sub_clinic_admin/order/index' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const WAREHOUSE_FALLBACK = {
|
const WAREHOUSE_FALLBACK = {
|
||||||
key: 'warehouse',
|
key: 'warehouse',
|
||||||
label: '仓库管理',
|
label: '仓库管理',
|
||||||
icon: '仓', // 将兜底图标改为名字首字
|
desc: '库存管理中心',
|
||||||
|
icon: 'car-fill',
|
||||||
|
theme: { bg: 'linear-gradient(135deg, #F3F4F6, #E5E7EB)', color: '#6B7280' }, // 占位灰
|
||||||
path: '/subPackages/sub_clinic_admin/warehouse/index',
|
path: '/subPackages/sub_clinic_admin/warehouse/index',
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,6 +95,8 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
storeName: '',
|
storeName: '',
|
||||||
|
storeQrCode: '',
|
||||||
|
storeQrData: null,
|
||||||
warehouseMenus: [WAREHOUSE_FALLBACK],
|
warehouseMenus: [WAREHOUSE_FALLBACK],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -88,21 +112,47 @@ export default {
|
|||||||
onShow() {
|
onShow() {
|
||||||
const user = uni.getStorageSync('clinic_admin_user') || {}
|
const user = uni.getStorageSync('clinic_admin_user') || {}
|
||||||
this.storeName = user.store_name || (user.store && user.store.name) || user.nick_name || '诊所管理'
|
this.storeName = user.store_name || (user.store && user.store.name) || user.nick_name || '诊所管理'
|
||||||
|
this.loadStoreQr()
|
||||||
this.loadWarehouseMenus()
|
this.loadWarehouseMenus()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
async loadStoreQr() {
|
||||||
|
this.storeQrCode = ''
|
||||||
|
this.storeQrData = null
|
||||||
|
const wrap = await getClinicAdminStoreQr()
|
||||||
|
if (!wrap || !wrap.ok) return
|
||||||
|
const store = wrap.data || {}
|
||||||
|
if (!store.qr_code) return
|
||||||
|
this.storeQrData = store
|
||||||
|
this.storeQrCode = store.qr_code
|
||||||
|
if (store.name) this.storeName = store.name
|
||||||
|
},
|
||||||
|
openStoreQrCode() {
|
||||||
|
if (!this.storeQrCode || !this.storeQrData) {
|
||||||
|
uni.showToast({ title: '诊所尚未生成二维码', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const { type, payload } = buildClinicQrPayload(this.storeQrData)
|
||||||
|
openQrCodePreview({ type, payload })
|
||||||
|
},
|
||||||
loadWarehouseMenus() {
|
loadWarehouseMenus() {
|
||||||
getWarehouseProductTypeOptions().then(w => {
|
getWarehouseProductTypeOptions().then(w => {
|
||||||
const raw = w.data
|
const raw = w.data
|
||||||
const options = Array.isArray(raw) ? raw : []
|
const options = Array.isArray(raw) ? raw : []
|
||||||
const warehouseMenus = options.map((opt) => {
|
const warehouseMenus = options.map((opt, index) => {
|
||||||
const value = opt.value != null ? opt.value : opt.id
|
const value = opt.value != null ? opt.value : opt.id
|
||||||
const label = opt.label || opt.name || '仓库'
|
const label = opt.label || opt.name || '仓库'
|
||||||
const title = label.indexOf('仓库') >= 0 ? label : label
|
const title = label.indexOf('仓库') >= 0 ? label : label
|
||||||
|
|
||||||
|
// 为动态仓库分配不同的主题色
|
||||||
|
const theme = GLOBAL_THEMES[index % GLOBAL_THEMES.length]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
key: 'warehouse-' + value,
|
key: 'warehouse-' + value,
|
||||||
label: title,
|
label: title,
|
||||||
icon: label.charAt(0), // 提取仓库名称的第一个字符作为 Icon
|
desc: `${title}库存管理`, // 增强描述
|
||||||
|
icon: 'car-fill', // 统一使用车辆图标代表仓库物流
|
||||||
|
theme: theme,
|
||||||
path: '/subPackages/sub_clinic_admin/warehouse/index?type=' + encodeURIComponent(value) + '&label=' + encodeURIComponent(title),
|
path: '/subPackages/sub_clinic_admin/warehouse/index?type=' + encodeURIComponent(value) + '&label=' + encodeURIComponent(title),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -119,163 +169,163 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
/* 品牌主题色 */
|
/* 全局容器背景:更干净的高级浅灰底色 */
|
||||||
$theme-primary: #6acdbb;
|
|
||||||
$theme-dark: #4db6a8;
|
|
||||||
|
|
||||||
/* 页面底色采用极淡的灰底,用来衬托纯白面板 */
|
|
||||||
$color-page-bg: #F5F7F8;
|
|
||||||
$color-text-title: #222B2A;
|
|
||||||
$color-text-sub: #5B6B69;
|
|
||||||
|
|
||||||
.workspace-container {
|
.workspace-container {
|
||||||
/* 移除左右和顶部 padding,保留底部安全距离 */
|
padding-bottom: 80rpx;
|
||||||
padding: 0 0 60rpx;
|
background-color: #F4F6F8;
|
||||||
background-color: $color-page-bg;
|
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 顶部品牌卡片:通栏布局,移除圆角 */
|
/* ================= 极简顶部英雄区 ================= */
|
||||||
.brand-card {
|
.hero-section {
|
||||||
position: relative;
|
background: linear-gradient(135deg, #6acdbb 0%, #4db6a8 100%);
|
||||||
background: linear-gradient(135deg, $theme-primary 0%, $theme-dark 100%);
|
padding: 40rpx 40rpx 60rpx;
|
||||||
padding: 48rpx 40rpx 44rpx;
|
border-bottom-left-radius: 48rpx;
|
||||||
margin-bottom: 24rpx;
|
border-bottom-right-radius: 48rpx;
|
||||||
color: #ffffff;
|
box-shadow: 0 4rpx 20rpx rgba(77, 182, 168, 0.2);
|
||||||
box-shadow: 0 4rpx 16rpx rgba(106, 205, 187, 0.15);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-info {
|
.brand-info-wrap {
|
||||||
position: relative;
|
display: flex;
|
||||||
z-index: 2;
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-text {
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge {
|
.badge {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
background: rgba(255, 255, 255, 0.2);
|
background: rgba(255, 255, 255, 0.15);
|
||||||
border: 1rpx solid rgba(255, 255, 255, 0.3);
|
backdrop-filter: blur(8px);
|
||||||
padding: 6rpx 16rpx;
|
padding: 6rpx 20rpx;
|
||||||
border-radius: 30rpx;
|
border-radius: 30rpx;
|
||||||
margin-bottom: 24rpx;
|
margin-bottom: 20rpx;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-text {
|
.badge-text {
|
||||||
font-size: 20rpx;
|
font-size: 22rpx;
|
||||||
font-weight: 600;
|
color: #fff;
|
||||||
color: #ffffff;
|
font-weight: 500;
|
||||||
|
letter-spacing: 2rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.store-name {
|
.store-name {
|
||||||
font-size: 40rpx;
|
font-size: 42rpx;
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
display: block;
|
display: block;
|
||||||
line-height: 1.3;
|
margin-bottom: 6rpx;
|
||||||
margin-bottom: 8rpx;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.store-subtitle {
|
.store-sub {
|
||||||
font-size: 24rpx;
|
font-size: 26rpx;
|
||||||
opacity: 0.85;
|
color: rgba(255, 255, 255, 0.85);
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-decorator {
|
/* 二维码容器样式 */
|
||||||
position: absolute;
|
.qr-container {
|
||||||
right: -60rpx;
|
|
||||||
bottom: -60rpx;
|
|
||||||
width: 280rpx;
|
|
||||||
height: 280rpx;
|
|
||||||
border: 36rpx solid rgba(255, 255, 255, 0.06);
|
|
||||||
border-radius: 50%;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 聚合大面板:通栏布局,移除圆角 */
|
|
||||||
.section-panel {
|
|
||||||
background-color: #ffffff;
|
|
||||||
padding: 32rpx 32rpx 40rpx;
|
|
||||||
margin-bottom: 24rpx;
|
|
||||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.02);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 面板标题区 */
|
|
||||||
.panel-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0 8rpx;
|
|
||||||
margin-bottom: 32rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-title {
|
|
||||||
font-size: 30rpx;
|
|
||||||
font-weight: 700;
|
|
||||||
color: $color-text-title;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-dots {
|
|
||||||
display: flex;
|
|
||||||
gap: 6rpx;
|
|
||||||
|
|
||||||
.dot {
|
|
||||||
width: 8rpx;
|
|
||||||
height: 8rpx;
|
|
||||||
background-color: rgba(106, 205, 187, 0.3);
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 4列网格布局 */
|
|
||||||
.grid-layout {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(4, 1fr); /* 严格一行四列 */
|
|
||||||
row-gap: 40rpx; /* 行间距适中,列间距靠 fr 自动均分 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 单个网格元素 */
|
|
||||||
.grid-item {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
|
||||||
transition: opacity 0.2s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-item-active {
|
.qr-image, .qr-placeholder {
|
||||||
opacity: 0.6;
|
width: 110rpx;
|
||||||
}
|
height: 110rpx;
|
||||||
|
background: #fff;
|
||||||
/* 图标容器:采用类 iOS 的超椭圆,并透出主题色 */
|
border-radius: 24rpx;
|
||||||
.icon-container {
|
padding: 10rpx;
|
||||||
width: 88rpx;
|
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1);
|
||||||
height: 88rpx;
|
|
||||||
border-radius: 28rpx;
|
|
||||||
background: linear-gradient(135deg, rgba(106, 205, 187, 0.15), rgba(106, 205, 187, 0.05));
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-bottom: 16rpx;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-char {
|
.qr-hint {
|
||||||
font-size: 38rpx;
|
font-size: 20rpx;
|
||||||
color: $theme-dark;
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
margin-top: 12rpx;
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 4rpx 16rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================= 主体内容区:快捷服务 ================= */
|
||||||
|
.content-wrapper {
|
||||||
|
margin-top: 30rpx; /* 轻微向上偏移,与顶部区域产生连接感 */
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-panel {
|
||||||
|
margin: 0 30rpx 30rpx;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
padding: 0 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-title {
|
||||||
|
font-size: 34rpx;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
color: #1E293B; /* 深石板灰 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 文字标签:处理过长文本的截断 */
|
/* 宫格卡片布局:与平台端/推广员端一致的两列 */
|
||||||
.item-label {
|
.service-grid {
|
||||||
font-size: 24rpx;
|
display: grid;
|
||||||
color: $color-text-sub;
|
grid-template-columns: repeat(2, 1fr);
|
||||||
font-weight: 500;
|
gap: 20rpx;
|
||||||
text-align: center;
|
}
|
||||||
width: 100%;
|
|
||||||
padding: 0 8rpx;
|
.service-card {
|
||||||
box-sizing: border-box;
|
background: #FFFFFF;
|
||||||
white-space: nowrap;
|
border-radius: 28rpx;
|
||||||
overflow: hidden;
|
padding: 30rpx 24rpx;
|
||||||
text-overflow: ellipsis;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.02);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-card-hover {
|
||||||
|
transform: translateY(-2rpx);
|
||||||
|
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-icon-box {
|
||||||
|
width: 76rpx;
|
||||||
|
height: 76rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
box-shadow: inset 0 2rpx 4rpx rgba(255, 255, 255, 0.5); /* 顶部微高光,增加拟物质感 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-name {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1E293B;
|
||||||
|
margin-bottom: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-desc {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #94A3B8; /* 银灰色 */
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -18,13 +18,13 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 信息与设置面板(通栏) -->
|
<!-- <!– 信息与设置面板(通栏) –>-->
|
||||||
<view class="setting-panel">
|
<!-- <view class="setting-panel">-->
|
||||||
<view class="setting-item">
|
<!-- <view class="setting-item">-->
|
||||||
<text class="item-label">所属诊所</text>
|
<!-- <text class="item-label">所属诊所</text>-->
|
||||||
<text class="item-value">{{ storeName }}</text>
|
<!-- <text class="item-value">{{ storeName }}</text>-->
|
||||||
</view>
|
<!-- </view>-->
|
||||||
</view>
|
<!-- </view>-->
|
||||||
|
|
||||||
<!-- 操作面板(通栏) -->
|
<!-- 操作面板(通栏) -->
|
||||||
<view class="setting-panel">
|
<view class="setting-panel">
|
||||||
@@ -46,8 +46,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ClinicPageLayout from '../components/clinic-page-layout.vue'
|
import ClinicPageLayout from '@/subPackages/sub_clinic_admin/components/clinic-page-layout.vue'
|
||||||
import AccountSwitchEntry from '@/components/account-switch/account-switch-entry.vue'
|
import AccountSwitchEntry from '@/components/account-switch/account-switch-entry.vue'
|
||||||
|
import { clearLoginStorage } from '@/utils/loginSession.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { ClinicPageLayout, AccountSwitchEntry },
|
components: { ClinicPageLayout, AccountSwitchEntry },
|
||||||
@@ -76,9 +77,7 @@ export default {
|
|||||||
confirmColor: '#f53f3f', // 将确认按钮改为警示红,提升用户体验
|
confirmColor: '#f53f3f', // 将确认按钮改为警示红,提升用户体验
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
if (!res.confirm) return
|
if (!res.confirm) return
|
||||||
uni.removeStorageSync('clinic_admin_token')
|
clearLoginStorage()
|
||||||
uni.removeStorageSync('clinic_admin_user')
|
|
||||||
uni.removeStorageSync('loginMode')
|
|
||||||
uni.reLaunch({ url: '/pages/login/index' })
|
uni.reLaunch({ url: '/pages/login/index' })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapExpressDetail } from '../../common/display.js'
|
import { mapExpressDetail } from '@/subPackages/sub_clinic_admin/common/display.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ExpressTimeline',
|
name: 'ExpressTimeline',
|
||||||
|
|||||||
@@ -169,7 +169,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getPrescriptionDetail } from '@/api/clinicAdmin.js'
|
import { getPrescriptionDetail } from '@/api/clinicAdmin.js'
|
||||||
import { mapPrescriptionDetailView } from '../../common/display.js'
|
import { mapPrescriptionDetailView } from '@/subPackages/sub_clinic_admin/common/display.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'PrescriptionDetailPopup',
|
name: 'PrescriptionDetailPopup',
|
||||||
|
|||||||
@@ -137,7 +137,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ClinicPageLayout from '../components/clinic-page-layout.vue'
|
import ClinicPageLayout from '@/subPackages/sub_clinic_admin/components/clinic-page-layout.vue'
|
||||||
import ExpressTimeline from './components/ExpressTimeline.vue'
|
import ExpressTimeline from './components/ExpressTimeline.vue'
|
||||||
import PrescriptionDetailPopup from './components/PrescriptionDetailPopup.vue'
|
import PrescriptionDetailPopup from './components/PrescriptionDetailPopup.vue'
|
||||||
import {
|
import {
|
||||||
@@ -153,8 +153,8 @@ import {
|
|||||||
erpSyncText,
|
erpSyncText,
|
||||||
mapOrderDetailDisplay,
|
mapOrderDetailDisplay,
|
||||||
mapOrderProductItem,
|
mapOrderProductItem,
|
||||||
} from '../common/display.js'
|
} from '@/subPackages/sub_clinic_admin/common/display.js'
|
||||||
import { formatMoney } from '../common/format.js'
|
import { formatMoney } from '@/subPackages/sub_clinic_admin/common/format.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { ClinicPageLayout, ExpressTimeline, PrescriptionDetailPopup },
|
components: { ClinicPageLayout, ExpressTimeline, PrescriptionDetailPopup },
|
||||||
|
|||||||
@@ -135,24 +135,24 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ClinicPageLayout from '../components/clinic-page-layout.vue'
|
import ClinicPageLayout from '@/subPackages/sub_clinic_admin/components/clinic-page-layout.vue'
|
||||||
import PageStickyHeader from '../components/PageStickyHeader.vue'
|
import PageStickyHeader from '@/subPackages/sub_clinic_admin/components/PageStickyHeader.vue'
|
||||||
import CollapsibleFilterPanel from '../components/CollapsibleFilterPanel.vue'
|
import CollapsibleFilterPanel from '@/subPackages/sub_clinic_admin/components/CollapsibleFilterPanel.vue'
|
||||||
import PrescriptionDetailPopup from './components/PrescriptionDetailPopup.vue'
|
import PrescriptionDetailPopup from './components/PrescriptionDetailPopup.vue'
|
||||||
import { getOrderList, getOrderStatusOption, getOrderSaleAmount } from '@/api/clinicAdmin.js'
|
import { getOrderList, getOrderStatusOption, getOrderSaleAmount } from '@/api/clinicAdmin.js'
|
||||||
import { withWxKey } from '@/utils/wxListKey.js'
|
import { withWxKey } from '@/utils/wxListKey.js'
|
||||||
import { mapOrderListRow } from '../common/display.js'
|
import { mapOrderListRow } from '@/subPackages/sub_clinic_admin/common/display.js'
|
||||||
import { formatMoney } from '../common/format.js'
|
import { formatMoney } from '@/subPackages/sub_clinic_admin/common/format.js'
|
||||||
import {
|
import {
|
||||||
buildOrderListParams,
|
buildOrderListParams,
|
||||||
buildOrderSaleAmountParams,
|
buildOrderSaleAmountParams,
|
||||||
} from '../common/orderParams.js'
|
} from '@/subPackages/sub_clinic_admin/common/orderParams.js'
|
||||||
import {
|
import {
|
||||||
loadOrderFilter,
|
loadOrderFilter,
|
||||||
saveOrderFilter,
|
saveOrderFilter,
|
||||||
clearOrderFilter,
|
clearOrderFilter,
|
||||||
defaultOrderFilter,
|
defaultOrderFilter,
|
||||||
} from '../common/filterCache.js'
|
} from '@/subPackages/sub_clinic_admin/common/filterCache.js'
|
||||||
|
|
||||||
const DELIVERY_OPTIONS = [
|
const DELIVERY_OPTIONS = [
|
||||||
{ label: '全部', value: '' },
|
{ label: '全部', value: '' },
|
||||||
|
|||||||
@@ -152,9 +152,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ClinicPageLayout from '../../components/clinic-page-layout.vue'
|
import ClinicPageLayout from '@/subPackages/sub_clinic_admin/components/clinic-page-layout.vue'
|
||||||
import { formatMoney } from '../../common/format.js'
|
import { formatMoney } from '@/subPackages/sub_clinic_admin/common/format.js'
|
||||||
import { mapReconciliationOrderSummary } from '../../common/display.js'
|
import { mapReconciliationOrderSummary } from '@/subPackages/sub_clinic_admin/common/display.js'
|
||||||
import {
|
import {
|
||||||
getOrderTrace,
|
getOrderTrace,
|
||||||
getOrderLedgerDetail,
|
getOrderLedgerDetail,
|
||||||
|
|||||||
@@ -210,9 +210,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ClinicPageLayout from '../components/clinic-page-layout.vue'
|
import ClinicPageLayout from '@/subPackages/sub_clinic_admin/components/clinic-page-layout.vue'
|
||||||
import PageStickyHeader from '../components/PageStickyHeader.vue'
|
import PageStickyHeader from '@/subPackages/sub_clinic_admin/components/PageStickyHeader.vue'
|
||||||
import CollapsibleFilterPanel from '../components/CollapsibleFilterPanel.vue'
|
import CollapsibleFilterPanel from '@/subPackages/sub_clinic_admin/components/CollapsibleFilterPanel.vue'
|
||||||
import {
|
import {
|
||||||
getReconciliationList,
|
getReconciliationList,
|
||||||
getReconciliationDrugList,
|
getReconciliationDrugList,
|
||||||
@@ -225,19 +225,19 @@ import {
|
|||||||
mapReconciliationDrugRow,
|
mapReconciliationDrugRow,
|
||||||
mapReconciliationStoreCount,
|
mapReconciliationStoreCount,
|
||||||
mapReconciliationDrugCount,
|
mapReconciliationDrugCount,
|
||||||
} from '../common/display.js'
|
} from '@/subPackages/sub_clinic_admin/common/display.js'
|
||||||
import {
|
import {
|
||||||
buildStoreListParams,
|
buildStoreListParams,
|
||||||
buildDrugListParams,
|
buildDrugListParams,
|
||||||
buildSearchTimeQueryParams,
|
buildSearchTimeQueryParams,
|
||||||
normalizeReconciliationResponse,
|
normalizeReconciliationResponse,
|
||||||
} from '../common/reconciliationParams.js'
|
} from '@/subPackages/sub_clinic_admin/common/reconciliationParams.js'
|
||||||
import {
|
import {
|
||||||
loadReconciliationFilter,
|
loadReconciliationFilter,
|
||||||
saveReconciliationFilter,
|
saveReconciliationFilter,
|
||||||
clearReconciliationFilter,
|
clearReconciliationFilter,
|
||||||
defaultReconciliationFilter,
|
defaultReconciliationFilter,
|
||||||
} from '../common/filterCache.js'
|
} from '@/subPackages/sub_clinic_admin/common/filterCache.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { ClinicPageLayout, PageStickyHeader, CollapsibleFilterPanel },
|
components: { ClinicPageLayout, PageStickyHeader, CollapsibleFilterPanel },
|
||||||
|
|||||||
@@ -67,14 +67,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ClinicPageLayout from '../components/clinic-page-layout.vue'
|
import ClinicPageLayout from '@/subPackages/sub_clinic_admin/components/clinic-page-layout.vue'
|
||||||
import PriceEditModal from './components/PriceEditModal.vue'
|
import PriceEditModal from './components/PriceEditModal.vue'
|
||||||
import {
|
import {
|
||||||
getWarehouseDetail,
|
getWarehouseDetail,
|
||||||
updateWarehouseStatus,
|
updateWarehouseStatus,
|
||||||
updateWarehousePrice,
|
updateWarehousePrice,
|
||||||
} from '@/api/clinicAdmin.js'
|
} from '@/api/clinicAdmin.js'
|
||||||
import { mapWarehouseRow } from '../common/display.js'
|
import { mapWarehouseRow } from '@/subPackages/sub_clinic_admin/common/display.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { ClinicPageLayout, PriceEditModal },
|
components: { ClinicPageLayout, PriceEditModal },
|
||||||
|
|||||||
@@ -64,7 +64,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ClinicPageLayout from '../components/clinic-page-layout.vue'
|
import ClinicPageLayout from '@/subPackages/sub_clinic_admin/components/clinic-page-layout.vue'
|
||||||
import PriceEditModal from './components/PriceEditModal.vue'
|
import PriceEditModal from './components/PriceEditModal.vue'
|
||||||
import {
|
import {
|
||||||
getWarehouseList,
|
getWarehouseList,
|
||||||
@@ -72,7 +72,7 @@ import {
|
|||||||
updateWarehousePrice,
|
updateWarehousePrice,
|
||||||
} from '@/api/clinicAdmin.js'
|
} from '@/api/clinicAdmin.js'
|
||||||
import { withWxKey } from '@/utils/wxListKey.js'
|
import { withWxKey } from '@/utils/wxListKey.js'
|
||||||
import { mapWarehouseRow } from '../common/display.js'
|
import { mapWarehouseRow } from '@/subPackages/sub_clinic_admin/common/display.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { ClinicPageLayout, PriceEditModal },
|
components: { ClinicPageLayout, PriceEditModal },
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ClinicPageLayout from '../../components/clinic-page-layout.vue'
|
import ClinicPageLayout from '@/subPackages/sub_clinic_admin/components/clinic-page-layout.vue'
|
||||||
import { getAccountChangeLogList } from '@/api/clinicAdmin.js'
|
import { getAccountChangeLogList } from '@/api/clinicAdmin.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@@ -13,8 +13,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ClinicPageLayout from '../components/clinic-page-layout.vue'
|
import ClinicPageLayout from '@/subPackages/sub_clinic_admin/components/clinic-page-layout.vue'
|
||||||
import { formatMoney } from '../common/format.js'
|
import { formatMoney } from '@/subPackages/sub_clinic_admin/common/format.js'
|
||||||
import { getClinicAdminBalance, submitWithdrawal } from '@/api/clinicAdmin.js'
|
import { getClinicAdminBalance, submitWithdrawal } from '@/api/clinicAdmin.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ClinicPageLayout from '../components/clinic-page-layout.vue'
|
import ClinicPageLayout from '@/subPackages/sub_clinic_admin/components/clinic-page-layout.vue'
|
||||||
import { getClinicAdminCard, saveClinicAdminCard } from '@/api/clinicAdmin.js'
|
import { getClinicAdminCard, saveClinicAdminCard } from '@/api/clinicAdmin.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@@ -74,11 +74,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ClinicPageLayout from '../components/clinic-page-layout.vue'
|
import ClinicPageLayout from '@/subPackages/sub_clinic_admin/components/clinic-page-layout.vue'
|
||||||
import { getClinicAdminCard, getClinicAdminBalance, getWithdrawalList } from '@/api/clinicAdmin.js'
|
import { getClinicAdminCard, getClinicAdminBalance, getWithdrawalList } from '@/api/clinicAdmin.js'
|
||||||
import { withWxKey } from '@/utils/wxListKey.js'
|
import { withWxKey } from '@/utils/wxListKey.js'
|
||||||
import { formatMoney } from '../common/format.js'
|
import { formatMoney } from '@/subPackages/sub_clinic_admin/common/format.js'
|
||||||
import { withdrawalCheckStatusText, formatDateTime } from '../common/display.js'
|
import { withdrawalCheckStatusText, formatDateTime } from '@/subPackages/sub_clinic_admin/common/display.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { ClinicPageLayout },
|
components: { ClinicPageLayout },
|
||||||
|
|||||||
@@ -42,13 +42,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ClinicPageLayout from '../components/clinic-page-layout.vue'
|
import ClinicPageLayout from '@/subPackages/sub_clinic_admin/components/clinic-page-layout.vue'
|
||||||
import { formatMoney } from '../common/format.js'
|
import { formatMoney } from '@/subPackages/sub_clinic_admin/common/format.js'
|
||||||
import {
|
import {
|
||||||
formatDateTime,
|
formatDateTime,
|
||||||
withdrawalCheckStatusText,
|
withdrawalCheckStatusText,
|
||||||
dakuanStatusText as dakuanStatusLabel,
|
dakuanStatusText as dakuanStatusLabel,
|
||||||
} from '../common/display.js'
|
} from '@/subPackages/sub_clinic_admin/common/display.js'
|
||||||
import { getWithdrawalDetail } from '@/api/clinicAdmin.js'
|
import { getWithdrawalDetail } from '@/api/clinicAdmin.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@@ -37,8 +37,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ClinicPageLayout from '../../components/clinic-page-layout.vue'
|
import ClinicPageLayout from '@/subPackages/sub_clinic_admin/components/clinic-page-layout.vue'
|
||||||
import { formatMoney } from '../../common/format.js'
|
import { formatMoney } from '@/subPackages/sub_clinic_admin/common/format.js'
|
||||||
import { getLedgerDetail } from '@/api/clinicAdmin.js'
|
import { getLedgerDetail } from '@/api/clinicAdmin.js'
|
||||||
import { withWxKey } from '@/utils/wxListKey.js'
|
import { withWxKey } from '@/utils/wxListKey.js'
|
||||||
|
|
||||||
|
|||||||
@@ -71,8 +71,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ClinicPageLayout from '../../components/clinic-page-layout.vue'
|
import ClinicPageLayout from '@/subPackages/sub_clinic_admin/components/clinic-page-layout.vue'
|
||||||
import { mapSettlementRow } from '../../common/display.js'
|
import { mapSettlementRow } from '@/subPackages/sub_clinic_admin/common/display.js'
|
||||||
import { getLedgerLogList } from '@/api/clinicAdmin.js'
|
import { getLedgerLogList } from '@/api/clinicAdmin.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
49
subPackages/sub_clinic_salesperson/commission/index.vue
Normal file
49
subPackages/sub_clinic_salesperson/commission/index.vue
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<template>
|
||||||
|
<clinic-salesperson-page-layout title="分成记录" :show-back="true">
|
||||||
|
<view class="page">
|
||||||
|
<view v-if="!list.length" class="empty">暂无分成记录</view>
|
||||||
|
<view v-for="item in list" :key="item.id" class="row">
|
||||||
|
<view>
|
||||||
|
<text class="order">{{ item.order_no }}</text>
|
||||||
|
<text class="drug">{{ item.drug_name }} ×{{ item.qty }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="right">
|
||||||
|
<text class="amount">¥{{ item.commission_amount }}</text>
|
||||||
|
<text class="status">{{ item.settlement_id > 0 ? '已结算' : '待结算' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</clinic-salesperson-page-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ClinicSalespersonPageLayout from '@/subPackages/sub_clinic_salesperson/components/clinic-salesperson-page-layout.vue'
|
||||||
|
import { getClinicSalespersonCommissionList } from '@/api/clinicSalesperson.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { ClinicSalespersonPageLayout },
|
||||||
|
data() {
|
||||||
|
return { list: [] }
|
||||||
|
},
|
||||||
|
onLoad() {
|
||||||
|
this.loadList()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async loadList() {
|
||||||
|
const wrap = await getClinicSalespersonCommissionList({ page: 1, pageSize: 50 })
|
||||||
|
if (wrap && wrap.ok) this.list = wrap.data?.items || []
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page { padding: 24rpx; }
|
||||||
|
.empty { text-align: center; color: #999; padding: 80rpx 0; }
|
||||||
|
.row { background: #fff; border-radius: 12rpx; padding: 24rpx; margin-bottom: 16rpx; display: flex; justify-content: space-between; }
|
||||||
|
.order { display: block; font-size: 26rpx; font-weight: 600; }
|
||||||
|
.drug { font-size: 24rpx; color: #666; }
|
||||||
|
.right { text-align: right; }
|
||||||
|
.amount { display: block; font-size: 28rpx; color: #4db6a8; font-weight: 600; }
|
||||||
|
.status { font-size: 22rpx; color: #999; }
|
||||||
|
</style>
|
||||||
20
subPackages/sub_clinic_salesperson/common/context.js
Normal file
20
subPackages/sub_clinic_salesperson/common/context.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
const ROOT = '/subPackages/sub_clinic_salesperson';
|
||||||
|
|
||||||
|
export function getClinicSalespersonContext() {
|
||||||
|
return {
|
||||||
|
tabList: [
|
||||||
|
{
|
||||||
|
iconPath: 'home',
|
||||||
|
selectedIconPath: 'home-fill',
|
||||||
|
text: '工作台',
|
||||||
|
pagePath: `${ROOT}/home/index`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
iconPath: 'account',
|
||||||
|
selectedIconPath: 'account-fill',
|
||||||
|
text: '我的',
|
||||||
|
pagePath: `${ROOT}/my/index`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
<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 { getClinicSalespersonContext } from '@/subPackages/sub_clinic_salesperson/common/context.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ClinicSalespersonPageLayout',
|
||||||
|
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: getClinicSalespersonContext().tabList,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
bodyStyle() {
|
||||||
|
const top = this.statusBarHeight + this.navbarHeight;
|
||||||
|
let bottom = '32rpx';
|
||||||
|
if (this.showTabbar) {
|
||||||
|
// bottom = 'calc(100rpx + env(safe-area-inset-bottom))';
|
||||||
|
bottom = '-7rpx';
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
paddingTop: top + 'px',
|
||||||
|
paddingBottom: bottom,
|
||||||
|
minHeight: '100vh',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.statusBarHeight = this.$statusBarHeight || 0;
|
||||||
|
this.navbarHeight = this.$navbarHeight || 44;
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onTabChange(index) {
|
||||||
|
if (index === this.tabIndex) return;
|
||||||
|
const target = this.tabList[index];
|
||||||
|
if (target && target.pagePath) {
|
||||||
|
uni.reLaunch({ url: target.pagePath });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.clinic-layout {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f9fafb;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
45
subPackages/sub_clinic_salesperson/earnings/index.vue
Normal file
45
subPackages/sub_clinic_salesperson/earnings/index.vue
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<template>
|
||||||
|
<clinic-salesperson-page-layout title="我的收益" :show-back="true">
|
||||||
|
<view class="page">
|
||||||
|
<view class="card" v-for="item in cards" :key="item.label">
|
||||||
|
<text class="label">{{ item.label }}</text>
|
||||||
|
<text class="value">{{ item.value }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</clinic-salesperson-page-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ClinicSalespersonPageLayout from '@/subPackages/sub_clinic_salesperson/components/clinic-salesperson-page-layout.vue'
|
||||||
|
import { getClinicSalespersonMyInfo } from '@/api/clinicSalesperson.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { ClinicSalespersonPageLayout },
|
||||||
|
data() {
|
||||||
|
return { cards: [] }
|
||||||
|
},
|
||||||
|
onLoad() {
|
||||||
|
this.loadData()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async loadData() {
|
||||||
|
const wrap = await getClinicSalespersonMyInfo()
|
||||||
|
if (!wrap || !wrap.ok) return
|
||||||
|
const stats = wrap.data?.stats || {}
|
||||||
|
this.cards = [
|
||||||
|
{ label: '累计分成', value: `¥${stats.money || '0.00'}` },
|
||||||
|
{ label: '待结算', value: `¥${stats.pending_amount || '0.00'}` },
|
||||||
|
{ label: '已结算', value: `¥${stats.settled_amount || '0.00'}` },
|
||||||
|
{ label: '累计获客', value: `${stats.user_number || 0} 人` },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page { padding: 24rpx; }
|
||||||
|
.card { background: #fff; border-radius: 16rpx; padding: 32rpx; margin-bottom: 20rpx; display: flex; justify-content: space-between; }
|
||||||
|
.label { color: #666; font-size: 28rpx; }
|
||||||
|
.value { font-size: 32rpx; font-weight: 600; color: #333; }
|
||||||
|
</style>
|
||||||
498
subPackages/sub_clinic_salesperson/home/index.vue
Normal file
498
subPackages/sub_clinic_salesperson/home/index.vue
Normal file
@@ -0,0 +1,498 @@
|
|||||||
|
<template>
|
||||||
|
<clinic-salesperson-page-layout title="推广员工作台" :show-tabbar="true" :tab-index="0">
|
||||||
|
<view class="workspace-container">
|
||||||
|
|
||||||
|
<!-- 极简流线型顶部个人信息区 -->
|
||||||
|
<view class="hero-section">
|
||||||
|
<view class="brand-info-wrap">
|
||||||
|
<view class="brand-text">
|
||||||
|
<view class="badge"><text class="badge-text">推广员工作台</text></view>
|
||||||
|
<text class="store-name">{{ displayName }}</text>
|
||||||
|
<text class="store-sub">{{ storeName }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="qr-container" @click="openQrCode">
|
||||||
|
<image
|
||||||
|
v-if="qrCode"
|
||||||
|
class="qr-image"
|
||||||
|
:src="qrCode"
|
||||||
|
mode="aspectFit"
|
||||||
|
/>
|
||||||
|
<view v-else class="qr-placeholder">
|
||||||
|
<!-- 替换电子名片 emoji -->
|
||||||
|
<u-icon name="scan" size="48" color="#666"></u-icon>
|
||||||
|
</view>
|
||||||
|
<text class="qr-hint">电子名片</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 核心资产看板:深色高定质感 -->
|
||||||
|
<view class="asset-dashboard">
|
||||||
|
<!-- 主资产卡片 (深邃质感) -->
|
||||||
|
<view class="primary-asset-card">
|
||||||
|
<!-- 装饰性光晕 -->
|
||||||
|
<view class="glow-orb glow-top-right"></view>
|
||||||
|
<view class="glow-orb glow-bottom-left"></view>
|
||||||
|
|
||||||
|
<view class="card-content-relative">
|
||||||
|
<view class="asset-header">
|
||||||
|
<view class="asset-title-wrap">
|
||||||
|
<text class="asset-title">待结算收益 (元)</text>
|
||||||
|
<view class="asset-status-dot"></view>
|
||||||
|
</view>
|
||||||
|
<view class="asset-tag">即将入账</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="asset-amount-wrap">
|
||||||
|
<text class="asset-currency">¥</text>
|
||||||
|
<text class="asset-amount-huge">{{ stats.pending_amount || '0.00' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 副指标区:胶囊状悬浮卡片 -->
|
||||||
|
<view class="secondary-stats-row">
|
||||||
|
<view class="stat-capsule">
|
||||||
|
<view class="capsule-icon-wrap bg-blue">
|
||||||
|
<!-- 替换已结算 emoji -->
|
||||||
|
<u-icon name="rmb-circle-fill" size="36" color="#3B82F6"></u-icon>
|
||||||
|
</view>
|
||||||
|
<view class="capsule-text-wrap">
|
||||||
|
<text class="capsule-label">已结算</text>
|
||||||
|
<text class="capsule-value">{{ stats.settled_amount || '0.00' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="stat-capsule">
|
||||||
|
<view class="capsule-icon-wrap bg-purple">
|
||||||
|
<!-- 替换累计获客 emoji -->
|
||||||
|
<u-icon name="account-fill" size="36" color="#A855F7"></u-icon>
|
||||||
|
</view>
|
||||||
|
<view class="capsule-text-wrap">
|
||||||
|
<text class="capsule-label">累计获客</text>
|
||||||
|
<text class="capsule-value">{{ stats.user_number || 0 }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 快捷服务区:多彩微拟物质感 -->
|
||||||
|
<view class="section-panel">
|
||||||
|
<view class="panel-header">
|
||||||
|
<text class="panel-title">常用服务</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="service-grid">
|
||||||
|
<view
|
||||||
|
v-for="(item, index) in enrichedMenuItems"
|
||||||
|
:key="item.key"
|
||||||
|
class="service-card"
|
||||||
|
hover-class="service-card-hover"
|
||||||
|
@click="go(item.path)"
|
||||||
|
>
|
||||||
|
<view class="service-icon-box" :style="{ background: item.theme.bg }">
|
||||||
|
<!-- 替换为 uView 动态图标 -->
|
||||||
|
<u-icon :name="item.icon" size="38" :color="item.theme.color"></u-icon>
|
||||||
|
</view>
|
||||||
|
<view class="service-info">
|
||||||
|
<text class="service-name">{{ item.label }}</text>
|
||||||
|
<text class="service-desc">{{ item.desc }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
</view>
|
||||||
|
</clinic-salesperson-page-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ClinicSalespersonPageLayout from '@/subPackages/sub_clinic_salesperson/components/clinic-salesperson-page-layout.vue'
|
||||||
|
import { getClinicSalespersonMyInfo } from '@/api/clinicSalesperson.js'
|
||||||
|
import { openQrCodePreview, buildSalespersonQrPayload } from '@/utils/qrCodePreview.js'
|
||||||
|
|
||||||
|
const ROOT = '/subPackages/sub_clinic_salesperson'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { ClinicSalespersonPageLayout },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
displayName: '',
|
||||||
|
storeName: '',
|
||||||
|
qrCode: '',
|
||||||
|
profileInfo: null,
|
||||||
|
stats: {},
|
||||||
|
menuItems: [
|
||||||
|
{ key: 'earnings', label: '我的收益', path: `${ROOT}/earnings/index` },
|
||||||
|
{ key: 'leads', label: '获客记录', path: `${ROOT}/leads/index` },
|
||||||
|
{ key: 'commission', label: '分成记录', path: `${ROOT}/commission/index` },
|
||||||
|
{ key: 'settlement', label: '结算记录', path: `${ROOT}/settlement/index` },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
// 动态注入多彩主题和增强文案,使用 uView 图标名称
|
||||||
|
enrichedMenuItems() {
|
||||||
|
const themes = [
|
||||||
|
// 收益:使用钻石/钱包相关图标
|
||||||
|
{ bg: 'linear-gradient(135deg, #FFF7ED, #FFEDD5)', color: '#F97316', desc: '查看收益明细', icon: 'red-packet-fill' },
|
||||||
|
// 获客:使用目标/数据相关图标
|
||||||
|
{ bg: 'linear-gradient(135deg, #EFF6FF, #DBEAFE)', color: '#3B82F6', desc: '客户转化漏斗', icon: 'bag-fill' },
|
||||||
|
// 分成:使用图表/比例相关图标
|
||||||
|
{ bg: 'linear-gradient(135deg, #FAF5FF, #F3E8FF)', color: '#A855F7', desc: '分佣比例与记录', icon: 'red-packet-fill' },
|
||||||
|
// 结算:使用卡片/提现相关图标
|
||||||
|
{ bg: 'linear-gradient(135deg, #F0FDF4, #DCFCE7)', color: '#22C55E', desc: '提现与账单', icon: 'rmb-circle-fill' },
|
||||||
|
];
|
||||||
|
return this.menuItems.map((item, index) => ({
|
||||||
|
...item,
|
||||||
|
icon: themes[index % themes.length].icon,
|
||||||
|
desc: themes[index % themes.length].desc,
|
||||||
|
theme: themes[index % themes.length]
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onShow() {
|
||||||
|
this.loadInfo()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async loadInfo() {
|
||||||
|
const wrap = await getClinicSalespersonMyInfo()
|
||||||
|
if (!wrap || !wrap.ok) return
|
||||||
|
const info = wrap.data || {}
|
||||||
|
this.profileInfo = info
|
||||||
|
this.displayName = info.salesperson?.nick_name || '推广员'
|
||||||
|
this.storeName = info.store?.name || ''
|
||||||
|
this.qrCode = info.qr_code || ''
|
||||||
|
this.stats = info.stats || {}
|
||||||
|
uni.setStorageSync('clinic_salesperson_user', info)
|
||||||
|
},
|
||||||
|
openQrCode() {
|
||||||
|
if (!this.qrCode) {
|
||||||
|
uni.showToast({ title: '暂无推广二维码', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const { type, payload } = buildSalespersonQrPayload(this.profileInfo || {
|
||||||
|
qr_code: this.qrCode,
|
||||||
|
store: { name: this.storeName },
|
||||||
|
salesperson: { nick_name: this.displayName },
|
||||||
|
})
|
||||||
|
openQrCodePreview({ type, payload })
|
||||||
|
},
|
||||||
|
go(path) {
|
||||||
|
uni.navigateTo({ url: path })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
/* 全局容器背景 */
|
||||||
|
.workspace-container {
|
||||||
|
padding-bottom: 80rpx;
|
||||||
|
background-color: #F4F6F8; /* 更干净的高级浅灰底色 */
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================= 极简顶部英雄区 ================= */
|
||||||
|
.hero-section {
|
||||||
|
background: linear-gradient(135deg, #6acdbb 0%, #4db6a8 100%);
|
||||||
|
padding: 40rpx 40rpx 120rpx; /* 底部预留负边距空间 */
|
||||||
|
border-bottom-left-radius: 48rpx;
|
||||||
|
border-bottom-right-radius: 48rpx;
|
||||||
|
box-shadow: 0 4rpx 20rpx rgba(77, 182, 168, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-info-wrap {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-text {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
display: inline-flex;
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
padding: 6rpx 20rpx;
|
||||||
|
border-radius: 30rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-text {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 2rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-name {
|
||||||
|
font-size: 42rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-sub {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-image, .qr-placeholder {
|
||||||
|
width: 110rpx;
|
||||||
|
height: 110rpx;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 10rpx;
|
||||||
|
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移除了无用的 .qr-icon-placeholder 类 */
|
||||||
|
|
||||||
|
.qr-hint {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
margin-top: 12rpx;
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 4rpx 16rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================= 核心资产看板区 ================= */
|
||||||
|
.asset-dashboard {
|
||||||
|
margin: -80rpx 30rpx 40rpx;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主卡片:黑金/深色沉浸质感 */
|
||||||
|
.primary-asset-card {
|
||||||
|
position: relative;
|
||||||
|
background: linear-gradient(145deg, #1E293B 0%, #0F172A 100%);
|
||||||
|
border-radius: 32rpx;
|
||||||
|
padding: 40rpx;
|
||||||
|
box-shadow: 0 16rpx 40rpx rgba(15, 23, 42, 0.15);
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 抽象光晕装饰背景,提升高定感 */
|
||||||
|
.glow-orb {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
filter: blur(40px);
|
||||||
|
opacity: 0.4;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glow-top-right {
|
||||||
|
width: 200rpx;
|
||||||
|
height: 200rpx;
|
||||||
|
background: #4DB6A8;
|
||||||
|
top: -50rpx;
|
||||||
|
right: -50rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glow-bottom-left {
|
||||||
|
width: 150rpx;
|
||||||
|
height: 150rpx;
|
||||||
|
background: #3B82F6;
|
||||||
|
bottom: -40rpx;
|
||||||
|
left: -40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content-relative {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-title-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-title {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #94A3B8;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-status-dot {
|
||||||
|
width: 12rpx;
|
||||||
|
height: 12rpx;
|
||||||
|
background-color: #10B981;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-left: 12rpx;
|
||||||
|
box-shadow: 0 0 8rpx rgba(16, 185, 129, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-tag {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #F8FAFC;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
padding: 6rpx 16rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-amount-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
color: #F8FAFC; /* 亮白色金额 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-currency {
|
||||||
|
font-size: 40rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-right: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-amount-huge {
|
||||||
|
font-size: 80rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -2rpx;
|
||||||
|
font-family: "DIN Alternate", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
text-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 胶囊副指标区 */
|
||||||
|
.secondary-stats-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-capsule {
|
||||||
|
flex: 1;
|
||||||
|
background: #FFFFFF;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 24rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.capsule-icon-wrap {
|
||||||
|
width: 64rpx;
|
||||||
|
height: 64rpx;
|
||||||
|
border-radius: 18rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-blue { background: #EFF6FF; }
|
||||||
|
.bg-purple { background: #FAF5FF; }
|
||||||
|
/* 移除了无用的 .capsule-icon 类 */
|
||||||
|
|
||||||
|
.capsule-text-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.capsule-label {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #64748B;
|
||||||
|
margin-bottom: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.capsule-value {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0F172A;
|
||||||
|
font-family: "DIN Alternate", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================= 快捷服务区:多彩拟物 ================= */
|
||||||
|
.section-panel {
|
||||||
|
margin: 0 30rpx;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
padding: 0 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-title {
|
||||||
|
font-size: 34rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1E293B;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-card {
|
||||||
|
background: #FFFFFF;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
padding: 30rpx 24rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.02);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-card-hover {
|
||||||
|
transform: translateY(-2rpx);
|
||||||
|
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-icon-box {
|
||||||
|
width: 76rpx;
|
||||||
|
height: 76rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
box-shadow: inset 0 2rpx 4rpx rgba(255, 255, 255, 0.5); /* 顶部微高光,增加拟物质感 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移除了无用的 .service-icon 类 */
|
||||||
|
|
||||||
|
.service-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-name {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1E293B;
|
||||||
|
margin-bottom: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-desc {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #94A3B8;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
44
subPackages/sub_clinic_salesperson/leads/index.vue
Normal file
44
subPackages/sub_clinic_salesperson/leads/index.vue
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<template>
|
||||||
|
<clinic-salesperson-page-layout title="获客记录" :show-back="true">
|
||||||
|
<view class="page">
|
||||||
|
<view v-if="!list.length" class="empty">暂无获客记录</view>
|
||||||
|
<view v-for="item in list" :key="item.id" class="row">
|
||||||
|
<view>
|
||||||
|
<text class="name">{{ item.user_nick_name || '用户' }}</text>
|
||||||
|
<text class="phone">{{ item.user_phone }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="time">{{ item.bind_at_text }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</clinic-salesperson-page-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ClinicSalespersonPageLayout from '@/subPackages/sub_clinic_salesperson/components/clinic-salesperson-page-layout.vue'
|
||||||
|
import { getClinicSalespersonUserBindList } from '@/api/clinicSalesperson.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { ClinicSalespersonPageLayout },
|
||||||
|
data() {
|
||||||
|
return { list: [] }
|
||||||
|
},
|
||||||
|
onLoad() {
|
||||||
|
this.loadList()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async loadList() {
|
||||||
|
const wrap = await getClinicSalespersonUserBindList({ page: 1, pageSize: 50 })
|
||||||
|
if (wrap && wrap.ok) this.list = wrap.data?.items || []
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page { padding: 24rpx; }
|
||||||
|
.empty { text-align: center; color: #999; padding: 80rpx 0; }
|
||||||
|
.row { background: #fff; border-radius: 12rpx; padding: 24rpx; margin-bottom: 16rpx; display: flex; justify-content: space-between; align-items: center; }
|
||||||
|
.name { display: block; font-size: 28rpx; font-weight: 600; }
|
||||||
|
.phone { font-size: 24rpx; color: #666; }
|
||||||
|
.time { font-size: 22rpx; color: #999; }
|
||||||
|
</style>
|
||||||
215
subPackages/sub_clinic_salesperson/my/index.vue
Normal file
215
subPackages/sub_clinic_salesperson/my/index.vue
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
<template>
|
||||||
|
|
||||||
|
<clinic-salesperson-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>
|
||||||
|
|
||||||
|
<text v-if="phone" class="phone-line">{{ phone }}</text>
|
||||||
|
|
||||||
|
<text v-if="storeName" class="store-line">{{ storeName }}</text>
|
||||||
|
|
||||||
|
</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">
|
||||||
|
|
||||||
|
<account-switch-entry />
|
||||||
|
|
||||||
|
<view
|
||||||
|
|
||||||
|
class="setting-item action-item"
|
||||||
|
|
||||||
|
hover-class="item-hover"
|
||||||
|
|
||||||
|
:hover-stay-time="100"
|
||||||
|
|
||||||
|
@click="logout"
|
||||||
|
|
||||||
|
>
|
||||||
|
|
||||||
|
<text class="danger-text">退出登录</text>
|
||||||
|
|
||||||
|
<u-icon name="arrow-right" color="#C9CDD4" size="28" />
|
||||||
|
|
||||||
|
</view>
|
||||||
|
|
||||||
|
</view>
|
||||||
|
|
||||||
|
</view>
|
||||||
|
|
||||||
|
</clinic-salesperson-page-layout>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import ClinicSalespersonPageLayout from '@/subPackages/sub_clinic_salesperson/components/clinic-salesperson-page-layout.vue'
|
||||||
|
|
||||||
|
import AccountSwitchEntry from '@/components/account-switch/account-switch-entry.vue'
|
||||||
|
|
||||||
|
import { getClinicSalespersonMyInfo } from '@/api/clinicSalesperson.js'
|
||||||
|
|
||||||
|
import { clearLoginStorage } from '@/utils/loginSession.js'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
|
||||||
|
components: { ClinicSalespersonPageLayout, AccountSwitchEntry },
|
||||||
|
|
||||||
|
data() {
|
||||||
|
|
||||||
|
return {
|
||||||
|
|
||||||
|
displayName: '',
|
||||||
|
|
||||||
|
phone: '',
|
||||||
|
|
||||||
|
storeName: '',
|
||||||
|
|
||||||
|
loading: false,
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
|
||||||
|
avatarText() {
|
||||||
|
|
||||||
|
return (this.displayName || '推').slice(0, 1)
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
onShow() {
|
||||||
|
|
||||||
|
this.loadProfile()
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
|
||||||
|
async loadProfile() {
|
||||||
|
|
||||||
|
if (this.loading) return
|
||||||
|
|
||||||
|
this.loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
const wrap = await getClinicSalespersonMyInfo()
|
||||||
|
|
||||||
|
if (!wrap || !wrap.ok) return
|
||||||
|
|
||||||
|
const info = wrap.data || {}
|
||||||
|
|
||||||
|
this.displayName = info.salesperson?.nick_name || '推广员'
|
||||||
|
|
||||||
|
this.phone = info.salesperson?.phone || ''
|
||||||
|
|
||||||
|
this.storeName = info.store?.name || ''
|
||||||
|
|
||||||
|
uni.setStorageSync('clinic_salesperson_user', info)
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
|
||||||
|
this.loading = false
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
logout() {
|
||||||
|
|
||||||
|
uni.showModal({
|
||||||
|
|
||||||
|
title: '提示',
|
||||||
|
|
||||||
|
content: '确定退出登录?',
|
||||||
|
|
||||||
|
confirmColor: '#f53f3f',
|
||||||
|
|
||||||
|
success: (res) => {
|
||||||
|
|
||||||
|
if (!res.confirm) return
|
||||||
|
|
||||||
|
clearLoginStorage()
|
||||||
|
|
||||||
|
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-muted: #86909c;
|
||||||
|
|
||||||
|
$color-border: #f2f3f5;
|
||||||
|
|
||||||
|
$color-danger: #f53f3f;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.mine-container {
|
||||||
|
|
||||||
|
padding: 0 0 60rpx;
|
||||||
|
|
||||||
|
background-color: $color-page-bg;
|
||||||
54
subPackages/sub_clinic_salesperson/settlement/index.vue
Normal file
54
subPackages/sub_clinic_salesperson/settlement/index.vue
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<template>
|
||||||
|
<clinic-salesperson-page-layout title="结算记录" :show-back="true">
|
||||||
|
<view class="page">
|
||||||
|
<view v-if="!list.length" class="empty">暂无结算记录</view>
|
||||||
|
<view v-for="item in list" :key="item.id" class="row" @click="openDetail(item.id)">
|
||||||
|
<view>
|
||||||
|
<text class="no">{{ item.settlement_no }}</text>
|
||||||
|
<text class="time">{{ item.created_at_text }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="amount">¥{{ item.amount }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</clinic-salesperson-page-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ClinicSalespersonPageLayout from '@/subPackages/sub_clinic_salesperson/components/clinic-salesperson-page-layout.vue'
|
||||||
|
import { getClinicSalespersonSettlementList, getClinicSalespersonSettlementDetail } from '@/api/clinicSalesperson.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { ClinicSalespersonPageLayout },
|
||||||
|
data() {
|
||||||
|
return { list: [] }
|
||||||
|
},
|
||||||
|
onLoad() {
|
||||||
|
this.loadList()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async loadList() {
|
||||||
|
const wrap = await getClinicSalespersonSettlementList({ page: 1, pageSize: 50 })
|
||||||
|
if (wrap && wrap.ok) this.list = wrap.data?.items || []
|
||||||
|
},
|
||||||
|
async openDetail(id) {
|
||||||
|
const wrap = await getClinicSalespersonSettlementDetail(id)
|
||||||
|
if (!wrap || !wrap.ok) return
|
||||||
|
const d = wrap.data || {}
|
||||||
|
uni.showModal({
|
||||||
|
title: '结算详情',
|
||||||
|
content: `单号:${d.settlement_no}\n金额:¥${d.amount}\n明细:${d.record_count}条`,
|
||||||
|
showCancel: false,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page { padding: 24rpx; }
|
||||||
|
.empty { text-align: center; color: #999; padding: 80rpx 0; }
|
||||||
|
.row { background: #fff; border-radius: 12rpx; padding: 24rpx; margin-bottom: 16rpx; display: flex; justify-content: space-between; align-items: center; }
|
||||||
|
.no { display: block; font-size: 26rpx; font-weight: 600; }
|
||||||
|
.time { font-size: 22rpx; color: #999; }
|
||||||
|
.amount { font-size: 30rpx; color: #4db6a8; font-weight: 600; }
|
||||||
|
</style>
|
||||||
632
subPackages/sub_clinic_salesperson/utils/address-index.js
Normal file
632
subPackages/sub_clinic_salesperson/utils/address-index.js
Normal file
@@ -0,0 +1,632 @@
|
|||||||
|
import { addressOption } from '@/subPackages/sub_clinic_salesperson/utils/address.js';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/** 跨级搜索结果 */
|
||||||
|
|
||||||
|
|
||||||
|
/** 列表面板统一行 */
|
||||||
|
|
||||||
|
|
||||||
|
const MUNICIPALITY_LABELS = new Set([
|
||||||
|
'北京市',
|
||||||
|
'天津市',
|
||||||
|
'上海市',
|
||||||
|
'重庆市',
|
||||||
|
]);
|
||||||
|
|
||||||
|
let provinceIndexCache = null;
|
||||||
|
|
||||||
|
function isPrefectureCityLabel(label) {
|
||||||
|
return label.endsWith('市') && !label.endsWith('自治区');
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDistrictLikeLabel(label) {
|
||||||
|
return /(?:区|县|旗)$/.test(label);
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectMunicipality(province, children) {
|
||||||
|
if (MUNICIPALITY_LABELS.has(province.label)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!province.label.endsWith('市') || children.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const districtCount = children.filter((c) => isDistrictLikeLabel(c.label)).length;
|
||||||
|
return districtCount / children.length >= 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildProvinceIndexList() {
|
||||||
|
return (addressOption).map((province) => {
|
||||||
|
const children = (province)
|
||||||
|
.children ?? [];
|
||||||
|
return {
|
||||||
|
province: {
|
||||||
|
value: province.value,
|
||||||
|
label: province.label,
|
||||||
|
pvalue: province.pvalue,
|
||||||
|
},
|
||||||
|
children: children.map((c) => ({
|
||||||
|
value: c.value,
|
||||||
|
label: c.label,
|
||||||
|
pvalue: c.pvalue,
|
||||||
|
})),
|
||||||
|
isMunicipality: detectMunicipality(province, children),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProvinceIndexList() {
|
||||||
|
if (!provinceIndexCache) {
|
||||||
|
provinceIndexCache = buildProvinceIndexList();
|
||||||
|
}
|
||||||
|
return provinceIndexCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findProvinceIndex(provinceId) {
|
||||||
|
if (!provinceId) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return getProvinceIndexList().find((p) => p.province.value === provinceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findChild(
|
||||||
|
provinceIndex,
|
||||||
|
childId,
|
||||||
|
) {
|
||||||
|
if (!childId) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return provinceIndex.children.find((c) => c.value === childId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCityNamePrefix(label) {
|
||||||
|
return label.replace(/市$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 省级列表 */
|
||||||
|
export function getProvinces() {
|
||||||
|
return getProvinceIndexList().map((p) => p.province);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 市级列表(直辖市返回空) */
|
||||||
|
export function getCities(provinceId) {
|
||||||
|
const index = findProvinceIndex(provinceId);
|
||||||
|
if (!index || index.isMunicipality) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return index.children;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 区级列表 */
|
||||||
|
export function getDistricts(
|
||||||
|
provinceId,
|
||||||
|
cityId,
|
||||||
|
) {
|
||||||
|
const index = findProvinceIndex(provinceId);
|
||||||
|
if (!index) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (index.isMunicipality) {
|
||||||
|
return index.children;
|
||||||
|
}
|
||||||
|
if (!cityId) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const city = findChild(index, cityId);
|
||||||
|
if (!city || !isPrefectureCityLabel(city.label)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const prefix = getCityNamePrefix(city.label);
|
||||||
|
return index.children.filter(
|
||||||
|
(c) =>
|
||||||
|
c.value !== cityId &&
|
||||||
|
!isPrefectureCityLabel(c.label) &&
|
||||||
|
c.label.startsWith(prefix),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 当前层级候选列表 */
|
||||||
|
export function getLevelOptions(
|
||||||
|
level,
|
||||||
|
ctx,
|
||||||
|
) {
|
||||||
|
if (level === 'province') {
|
||||||
|
return getProvinces();
|
||||||
|
}
|
||||||
|
if (level === 'city') {
|
||||||
|
return getCities(ctx.provinceId);
|
||||||
|
}
|
||||||
|
return getDistricts(ctx.provinceId, ctx.cityId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 选择某级后的下一层级(无则 undefined) */
|
||||||
|
export function getNextLevel(
|
||||||
|
currentLevel,
|
||||||
|
ctx,
|
||||||
|
) {
|
||||||
|
const index = findProvinceIndex(ctx.provinceId);
|
||||||
|
if (!index) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (currentLevel === 'province') {
|
||||||
|
return index.isMunicipality ? 'district' : 'city';
|
||||||
|
}
|
||||||
|
if (currentLevel === 'city') {
|
||||||
|
if (!ctx.cityId) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const districts = getDistricts(ctx.provinceId, ctx.cityId);
|
||||||
|
return districts.length > 0 ? 'district' : undefined;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 当前层级选中后是否可直接完成(叶节点) */
|
||||||
|
export function canCompleteAfterSelect(
|
||||||
|
level,
|
||||||
|
ctx,
|
||||||
|
item,
|
||||||
|
) {
|
||||||
|
const index = findProvinceIndex(ctx.provinceId);
|
||||||
|
if (!index) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (level === 'district') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (level === 'city') {
|
||||||
|
if (index.isMunicipality) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!isPrefectureCityLabel(item.label)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return getDistricts(ctx.provinceId, item.value).length === 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 组装对外 address 值 */
|
||||||
|
export function buildAddressOutput(
|
||||||
|
provinceId,
|
||||||
|
cityId,
|
||||||
|
districtId,
|
||||||
|
) {
|
||||||
|
if (!provinceId) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const leafId = districtId ?? cityId;
|
||||||
|
if (!leafId) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return [provinceId, leafId];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 由 address 反查省/市/区 */
|
||||||
|
export function resolveAddressValue(
|
||||||
|
value,
|
||||||
|
) {
|
||||||
|
if (!value || value.length < 2) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const [provinceId, leafId] = value;
|
||||||
|
const index = findProvinceIndex(provinceId);
|
||||||
|
if (!index) {
|
||||||
|
return { provinceId };
|
||||||
|
}
|
||||||
|
const leaf = findChild(index, leafId);
|
||||||
|
if (!leaf) {
|
||||||
|
return { provinceId, province: index.province };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index.isMunicipality) {
|
||||||
|
return {
|
||||||
|
provinceId,
|
||||||
|
districtId: leafId,
|
||||||
|
province: index.province,
|
||||||
|
district: leaf,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLeafDistrict =
|
||||||
|
!isPrefectureCityLabel(leaf.label) && isDistrictLikeLabel(leaf.label);
|
||||||
|
|
||||||
|
if (isPrefectureCityLabel(leaf.label)) {
|
||||||
|
return {
|
||||||
|
provinceId,
|
||||||
|
cityId: leafId,
|
||||||
|
province: index.province,
|
||||||
|
city: leaf,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLeafDistrict) {
|
||||||
|
for (const city of index.children.filter((c) =>
|
||||||
|
isPrefectureCityLabel(c.label),
|
||||||
|
)) {
|
||||||
|
const prefix = getCityNamePrefix(city.label);
|
||||||
|
if (leaf.label.startsWith(prefix)) {
|
||||||
|
return {
|
||||||
|
provinceId,
|
||||||
|
cityId: city.value,
|
||||||
|
districtId: leafId,
|
||||||
|
province: index.province,
|
||||||
|
city,
|
||||||
|
district: leaf,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
provinceId,
|
||||||
|
cityId: leafId,
|
||||||
|
province: index.province,
|
||||||
|
city: leaf,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
provinceId,
|
||||||
|
cityId: leafId,
|
||||||
|
province: index.province,
|
||||||
|
city: leaf,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 面包屑展示项 */
|
||||||
|
export function formatBreadcrumbItems(
|
||||||
|
resolved,
|
||||||
|
) {
|
||||||
|
const items = [];
|
||||||
|
if (resolved.provinceId && resolved.province) {
|
||||||
|
items.push({
|
||||||
|
level: 'province',
|
||||||
|
id: resolved.provinceId,
|
||||||
|
label: resolved.province.label,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const index = findProvinceIndex(resolved.provinceId);
|
||||||
|
if (index?.isMunicipality) {
|
||||||
|
if (resolved.districtId && resolved.district) {
|
||||||
|
items.push({
|
||||||
|
level: 'district',
|
||||||
|
id: resolved.districtId,
|
||||||
|
label: resolved.district.label,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
if (resolved.cityId && resolved.city) {
|
||||||
|
items.push({
|
||||||
|
level: 'city',
|
||||||
|
id: resolved.cityId,
|
||||||
|
label: resolved.city.label,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (resolved.districtId && resolved.district) {
|
||||||
|
items.push({
|
||||||
|
level: 'district',
|
||||||
|
id: resolved.districtId,
|
||||||
|
label: resolved.district.label,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 触发器展示文案 */
|
||||||
|
export function formatAddressDisplay(value) {
|
||||||
|
const items = formatBreadcrumbItems(resolveAddressValue(value));
|
||||||
|
return items.map((i) => i.label).join(' / ');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 当前层级搜索占位 */
|
||||||
|
export function getLevelSearchPlaceholder(level) {
|
||||||
|
const map = {
|
||||||
|
province: '搜索省',
|
||||||
|
city: '搜索市/县',
|
||||||
|
district: '搜索区',
|
||||||
|
};
|
||||||
|
return map[level];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 模糊筛选 */
|
||||||
|
export function filterAddressOptions(
|
||||||
|
options,
|
||||||
|
keyword,
|
||||||
|
) {
|
||||||
|
const q = keyword.trim().toLowerCase();
|
||||||
|
if (!q) {
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
return options.filter((o) => o.label.toLowerCase().includes(q));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 从已选上下文生成面包屑(面板内) */
|
||||||
|
export function buildBreadcrumbFromContext(
|
||||||
|
ctx,
|
||||||
|
) {
|
||||||
|
return formatBreadcrumbItems({
|
||||||
|
provinceId: ctx.provinceId,
|
||||||
|
cityId: ctx.cityId,
|
||||||
|
districtId: ctx.districtId,
|
||||||
|
province: findProvinceIndex(ctx.provinceId)?.province,
|
||||||
|
city: findChild(findProvinceIndex(ctx.provinceId), ctx.cityId),
|
||||||
|
district: findChild(findProvinceIndex(ctx.provinceId), ctx.districtId),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 根据面板内选中生成 ResolvedAddress */
|
||||||
|
export function contextToResolved(ctx) {
|
||||||
|
const index = findProvinceIndex(ctx.provinceId);
|
||||||
|
return {
|
||||||
|
provinceId: ctx.provinceId,
|
||||||
|
cityId: ctx.cityId,
|
||||||
|
districtId: ctx.districtId,
|
||||||
|
province: index?.province,
|
||||||
|
city: index ? findChild(index, ctx.cityId) : undefined,
|
||||||
|
district: index ? findChild(index, ctx.districtId) : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 是否直辖市 */
|
||||||
|
export function isMunicipalityProvince(provinceId) {
|
||||||
|
return !!findProvinceIndex(provinceId)?.isMunicipality;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 打开面板时的初始层级(有回填则进入待选级,否则省) */
|
||||||
|
export function getInitialPickerLevel(
|
||||||
|
resolved,
|
||||||
|
) {
|
||||||
|
const index = findProvinceIndex(resolved.provinceId);
|
||||||
|
if (!resolved.provinceId || !index) {
|
||||||
|
return 'province';
|
||||||
|
}
|
||||||
|
if (index.isMunicipality) {
|
||||||
|
return 'district';
|
||||||
|
}
|
||||||
|
if (!resolved.cityId) {
|
||||||
|
return 'city';
|
||||||
|
}
|
||||||
|
const districts = getDistricts(resolved.provinceId, resolved.cityId);
|
||||||
|
if (!resolved.districtId && districts.length > 0) {
|
||||||
|
return 'district';
|
||||||
|
}
|
||||||
|
return 'city';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 当前级候选转列表行 */
|
||||||
|
export function levelOptionsToRows(
|
||||||
|
level,
|
||||||
|
ctx,
|
||||||
|
) {
|
||||||
|
return getLevelOptions(level, ctx).map((node) => ({
|
||||||
|
level,
|
||||||
|
node,
|
||||||
|
label: node.label,
|
||||||
|
complete: canCompleteAfterSelect(
|
||||||
|
level,
|
||||||
|
level === 'city'
|
||||||
|
? { provinceId: ctx.provinceId, cityId: node.value }
|
||||||
|
: level === 'district'
|
||||||
|
? {
|
||||||
|
provinceId: ctx.provinceId,
|
||||||
|
cityId: ctx.cityId,
|
||||||
|
districtId: node.value,
|
||||||
|
}
|
||||||
|
: ctx,
|
||||||
|
node,
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 跨级模糊搜索(含第三级区/县) */
|
||||||
|
export function searchAddressHits(
|
||||||
|
keyword,
|
||||||
|
ctx,
|
||||||
|
) {
|
||||||
|
const q = keyword.trim().toLowerCase();
|
||||||
|
if (!q) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const hits = [];
|
||||||
|
const seen = new Set();
|
||||||
|
|
||||||
|
const push = (hit) => {
|
||||||
|
const key = `${hit.level}-${hit.node.value}`;
|
||||||
|
if (!seen.has(key)) {
|
||||||
|
seen.add(key);
|
||||||
|
hits.push(hit);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!ctx.provinceId) {
|
||||||
|
for (const p of getProvinces()) {
|
||||||
|
if (p.label.toLowerCase().includes(q)) {
|
||||||
|
push({
|
||||||
|
level: 'province',
|
||||||
|
node: p,
|
||||||
|
pathLabel: p.label,
|
||||||
|
complete: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hits;
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = findProvinceIndex(ctx.provinceId);
|
||||||
|
if (!index) {
|
||||||
|
return hits;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index.isMunicipality) {
|
||||||
|
for (const d of index.children) {
|
||||||
|
if (d.label.toLowerCase().includes(q)) {
|
||||||
|
push({
|
||||||
|
level: 'district',
|
||||||
|
node: d,
|
||||||
|
pathLabel: `${index.province.label} / ${d.label}`,
|
||||||
|
complete: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hits;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cityNode = ctx.cityId ? findChild(index, ctx.cityId) : undefined;
|
||||||
|
|
||||||
|
if (!ctx.cityId) {
|
||||||
|
for (const child of index.children) {
|
||||||
|
if (!child.label.toLowerCase().includes(q)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const rowCtx = { provinceId: ctx.provinceId, cityId: child.value };
|
||||||
|
push({
|
||||||
|
level: 'city',
|
||||||
|
node: child,
|
||||||
|
pathLabel: `${index.province.label} / ${child.label}`,
|
||||||
|
complete: canCompleteAfterSelect('city', rowCtx, child),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const child of index.children.filter((c) => isPrefectureCityLabel(c.label))) {
|
||||||
|
for (const d of getDistricts(ctx.provinceId, child.value)) {
|
||||||
|
if (!d.label.toLowerCase().includes(q)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
push({
|
||||||
|
level: 'district',
|
||||||
|
node: d,
|
||||||
|
pathLabel: `${index.province.label} / ${child.label} / ${d.label}`,
|
||||||
|
complete: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hits;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cityNode && cityNode.label.toLowerCase().includes(q)) {
|
||||||
|
push({
|
||||||
|
level: 'city',
|
||||||
|
node: cityNode,
|
||||||
|
pathLabel: `${index.province.label} / ${cityNode.label}`,
|
||||||
|
complete: canCompleteAfterSelect('city', ctx, cityNode),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const d of getDistricts(ctx.provinceId, ctx.cityId)) {
|
||||||
|
if (d.label.toLowerCase().includes(q)) {
|
||||||
|
push({
|
||||||
|
level: 'district',
|
||||||
|
node: d,
|
||||||
|
pathLabel: `${index.province.label} / ${cityNode?.label ?? ''} / ${d.label}`,
|
||||||
|
complete: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hits;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 搜索命中转列表行 */
|
||||||
|
export function searchHitsToRows(hits) {
|
||||||
|
return hits.map((hit) => ({
|
||||||
|
level: hit.level,
|
||||||
|
node: hit.node,
|
||||||
|
label: hit.pathLabel,
|
||||||
|
complete: hit.complete,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 应用跨级搜索选中 */
|
||||||
|
export function applySearchHit(
|
||||||
|
hit,
|
||||||
|
ctx,
|
||||||
|
) {
|
||||||
|
if (hit.level === 'province') {
|
||||||
|
return {
|
||||||
|
provinceId: hit.node.value,
|
||||||
|
cityId: undefined,
|
||||||
|
districtId: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (hit.level === 'city') {
|
||||||
|
return {
|
||||||
|
provinceId: ctx.provinceId,
|
||||||
|
cityId: hit.node.value,
|
||||||
|
districtId: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (hit.level === 'district' && findProvinceIndex(ctx.provinceId)?.isMunicipality) {
|
||||||
|
return {
|
||||||
|
provinceId: ctx.provinceId,
|
||||||
|
cityId: undefined,
|
||||||
|
districtId: hit.node.value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
provinceId: ctx.provinceId,
|
||||||
|
cityId: ctx.cityId,
|
||||||
|
districtId: hit.node.value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 高亮项是否有下一级 */
|
||||||
|
export function rowHasNextLevel(
|
||||||
|
row,
|
||||||
|
ctx,
|
||||||
|
) {
|
||||||
|
if (row.level === 'province') {
|
||||||
|
return !!getNextLevel('province', {
|
||||||
|
provinceId: row.node.value,
|
||||||
|
cityId: undefined,
|
||||||
|
districtId: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (row.level === 'city') {
|
||||||
|
return !!getNextLevel('city', {
|
||||||
|
provinceId: ctx.provinceId,
|
||||||
|
cityId: row.node.value,
|
||||||
|
districtId: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 点击面包屑项后截断选中并返回新上下文 */
|
||||||
|
export function truncateContextToBreadcrumb(
|
||||||
|
ctx,
|
||||||
|
item,
|
||||||
|
) {
|
||||||
|
if (item.level === 'province') {
|
||||||
|
return {
|
||||||
|
provinceId: item.id,
|
||||||
|
cityId: undefined,
|
||||||
|
districtId: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (item.level === 'city') {
|
||||||
|
return {
|
||||||
|
provinceId: ctx.provinceId,
|
||||||
|
cityId: item.id,
|
||||||
|
districtId: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
provinceId: ctx.provinceId,
|
||||||
|
cityId: ctx.cityId,
|
||||||
|
districtId: item.id,
|
||||||
|
};
|
||||||
|
}
|
||||||
2
subPackages/sub_clinic_salesperson/utils/address.js
Normal file
2
subPackages/sub_clinic_salesperson/utils/address.js
Normal file
File diff suppressed because one or more lines are too long
@@ -323,10 +323,10 @@ import {
|
|||||||
extractSendToUserMessageId
|
extractSendToUserMessageId
|
||||||
} from '@/api/chat.js';
|
} from '@/api/chat.js';
|
||||||
import { parseMessageContent } from '../utils/messageParse.js';
|
import { parseMessageContent } from '../utils/messageParse.js';
|
||||||
import PatientDetailPanel from '../components/PatientDetailPanel.vue';
|
import PatientDetailPanel from '@/subPackages/sub_online_reception/components/PatientDetailPanel.vue';
|
||||||
import MessageBubble from '../components/MessageBubble.vue';
|
import MessageBubble from '@/subPackages/sub_online_reception/components/MessageBubble.vue';
|
||||||
import ReceptionActionBar from '../components/ReceptionActionBar.vue';
|
import ReceptionActionBar from '@/subPackages/sub_online_reception/components/ReceptionActionBar.vue';
|
||||||
import RefuseReceptionModal from '../components/RefuseReceptionModal.vue';
|
import RefuseReceptionModal from '@/subPackages/sub_online_reception/components/RefuseReceptionModal.vue';
|
||||||
import { withWxKey, ensureWxKey } from '@/utils/wxListKey.js';
|
import { withWxKey, ensureWxKey } from '@/utils/wxListKey.js';
|
||||||
import { prepareImagePath } from '@/common/js/image-compress.js';
|
import { prepareImagePath } from '@/common/js/image-compress.js';
|
||||||
|
|
||||||
|
|||||||
26
subPackages/sub_platform_admin/common/businessMixin.js
Normal file
26
subPackages/sub_platform_admin/common/businessMixin.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { resolveBusinessContext, pageUrl, businessPageRoot } from './context.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
inject: {
|
||||||
|
businessContext: { default: null },
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
bizCtx() {
|
||||||
|
return resolveBusinessContext(this.businessContext);
|
||||||
|
},
|
||||||
|
bizApi() {
|
||||||
|
return this.bizCtx.api;
|
||||||
|
},
|
||||||
|
bizCap() {
|
||||||
|
return this.bizCtx.capabilities || {};
|
||||||
|
},
|
||||||
|
bizRoot() {
|
||||||
|
return businessPageRoot(this.bizCtx);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
bizPage(subPath) {
|
||||||
|
return pageUrl(this.bizCtx, subPath);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
49
subPackages/sub_platform_admin/common/context.js
Normal file
49
subPackages/sub_platform_admin/common/context.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { platformAdminApi } from '@/api/businessApi.js';
|
||||||
|
|
||||||
|
const BUSINESS_PACKAGE_ROOT = '/subPackages/sub_business_shared';
|
||||||
|
const PLATFORM_HOME = '/subPackages/sub_platform_admin/home/index';
|
||||||
|
const PLATFORM_MY = '/subPackages/sub_platform_admin/my/index';
|
||||||
|
|
||||||
|
export function getPlatformAdminContext() {
|
||||||
|
return {
|
||||||
|
mode: 'platform_admin',
|
||||||
|
api: platformAdminApi,
|
||||||
|
userStorageKey: 'platform_admin_user',
|
||||||
|
homePath: PLATFORM_HOME,
|
||||||
|
myPath: PLATFORM_MY,
|
||||||
|
packageRoot: '/subPackages/sub_platform_admin',
|
||||||
|
businessPackageRoot: BUSINESS_PACKAGE_ROOT,
|
||||||
|
brandTitle: '平台管理工作台',
|
||||||
|
capabilities: {
|
||||||
|
canShip: true,
|
||||||
|
canRefund: true,
|
||||||
|
canStoreFilter: true,
|
||||||
|
canWithdraw: true,
|
||||||
|
canWithdrawAudit: true,
|
||||||
|
canWarehouse: true,
|
||||||
|
warehouseType: 'platform',
|
||||||
|
canStoreInput: true,
|
||||||
|
canDoctorInput: true,
|
||||||
|
canStoreInputAudit: true,
|
||||||
|
canDoctorInputAudit: true,
|
||||||
|
},
|
||||||
|
tabList: [
|
||||||
|
{ text: '工作台', iconPath: '/static/tabbar/home.png', selectedIconPath: '/static/tabbar/home-active.png', pagePath: PLATFORM_HOME },
|
||||||
|
{ text: '我的', iconPath: '/static/tabbar/my.png', selectedIconPath: '/static/tabbar/my-active.png', pagePath: PLATFORM_MY },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveBusinessContext(injected) {
|
||||||
|
if (injected && injected.api) return injected;
|
||||||
|
if (uni.getStorageSync('loginMode') === 'platform_admin') return getPlatformAdminContext();
|
||||||
|
return getPlatformAdminContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function businessPageRoot(ctx) {
|
||||||
|
return ctx.businessPackageRoot || ctx.packageRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pageUrl(ctx, subPath) {
|
||||||
|
return `${businessPageRoot(ctx)}/${subPath}`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
<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 businessMixin from '@/subPackages/sub_platform_admin/common/businessMixin.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'BusinessPageLayout',
|
||||||
|
mixins: [businessMixin],
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
tabList() {
|
||||||
|
return this.bizCtx.tabList || [];
|
||||||
|
},
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onTabChange(index) {
|
||||||
|
if (index === this.tabIndex) return;
|
||||||
|
const target = this.tabList[index];
|
||||||
|
if (target && target.pagePath) {
|
||||||
|
uni.reLaunch({ url: target.pagePath });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.clinic-layout {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f9fafb;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
17
subPackages/sub_platform_admin/doctor-input/audit/detail.vue
Normal file
17
subPackages/sub_platform_admin/doctor-input/audit/detail.vue
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<template>
|
||||||
|
<input-detail-page :record-id="recordId" input-type="doctor" />
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import InputDetailPage from '@/subPackages/sub_platform_admin/input/InputDetailPage.vue'
|
||||||
|
import { getPlatformAdminContext } from '@/subPackages/sub_platform_admin/common/context.js'
|
||||||
|
export default {
|
||||||
|
components: { InputDetailPage },
|
||||||
|
provide() { return { businessContext: getPlatformAdminContext() } },
|
||||||
|
data() {
|
||||||
|
return { recordId: 0 }
|
||||||
|
},
|
||||||
|
onLoad(q) {
|
||||||
|
this.recordId = Number(q.id || 0)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
24
subPackages/sub_platform_admin/doctor-input/audit/index.vue
Normal file
24
subPackages/sub_platform_admin/doctor-input/audit/index.vue
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<template>
|
||||||
|
<input-list-page
|
||||||
|
ref="listPage"
|
||||||
|
:audit-mode="true"
|
||||||
|
input-type="doctor"
|
||||||
|
form-path=""
|
||||||
|
detail-path="/subPackages/sub_platform_admin/doctor-input/audit/detail"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import InputListPage from '@/subPackages/sub_platform_admin/input/InputListPage.vue'
|
||||||
|
import { getPlatformAdminContext } from '@/subPackages/sub_platform_admin/common/context.js'
|
||||||
|
export default {
|
||||||
|
components: { InputListPage },
|
||||||
|
provide() { return { businessContext: getPlatformAdminContext() } },
|
||||||
|
onShow() {
|
||||||
|
this.$nextTick(() => this.$refs.listPage?.reload?.())
|
||||||
|
},
|
||||||
|
onReachBottom() {
|
||||||
|
const p = this.$refs.listPage
|
||||||
|
if (p) p.load(p.page + 1, true)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
340
subPackages/sub_platform_admin/home/index.vue
Normal file
340
subPackages/sub_platform_admin/home/index.vue
Normal file
@@ -0,0 +1,340 @@
|
|||||||
|
<template>
|
||||||
|
<business-page-layout title="工作台" :show-tabbar="true" :tab-index="0">
|
||||||
|
<view class="workspace-container">
|
||||||
|
|
||||||
|
<!-- 极简流线型顶部个人信息区 -->
|
||||||
|
<view class="hero-section">
|
||||||
|
<view class="brand-info-wrap">
|
||||||
|
<view class="brand-text">
|
||||||
|
<view class="badge"><text class="badge-text">平台管理工作台</text></view>
|
||||||
|
<text class="store-name">{{ displayName }}</text>
|
||||||
|
<text class="store-sub">欢迎回来,今天也要高效工作</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 快捷服务区:多彩微拟物质感 (复用样式逻辑) -->
|
||||||
|
<view class="content-wrapper">
|
||||||
|
<view v-for="section in sections" :key="section.key" class="section-panel">
|
||||||
|
<view class="panel-header">
|
||||||
|
<text class="panel-title">{{ section.title }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="service-grid">
|
||||||
|
<view
|
||||||
|
v-for="item in section.items"
|
||||||
|
:key="item.key"
|
||||||
|
class="service-card"
|
||||||
|
hover-class="service-card-hover"
|
||||||
|
@click="go(item)"
|
||||||
|
>
|
||||||
|
<view class="service-icon-box" :style="{ background: item.theme.bg }">
|
||||||
|
<!-- 使用 uView 1.x 的 u-icon 组件 -->
|
||||||
|
<u-icon :name="item.icon" size="38" :color="item.theme.color"></u-icon>
|
||||||
|
</view>
|
||||||
|
<view class="service-info">
|
||||||
|
<text class="service-name">{{ item.label }}</text>
|
||||||
|
<text class="service-desc">{{ item.desc || '快捷进入' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
</view>
|
||||||
|
</business-page-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import BusinessPageLayout from '@/subPackages/sub_platform_admin/components/business-page-layout.vue'
|
||||||
|
import { getPlatformAdminContext } from '@/subPackages/sub_platform_admin/common/context.js'
|
||||||
|
import { navigateToInputForm } from '@/subPackages/sub_salesperson/common/inputNavigation.js'
|
||||||
|
import platformAdminApi from '@/api/platformAdmin.js'
|
||||||
|
|
||||||
|
const ROOT = '/subPackages/sub_platform_admin'
|
||||||
|
const SALESPERSON_ROOT = '/subPackages/sub_salesperson'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { BusinessPageLayout },
|
||||||
|
provide() { return { businessContext: getPlatformAdminContext() } },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
displayName: '',
|
||||||
|
warehouseMenus: [],
|
||||||
|
// 预定义的全局主题池,用于动态分配没有明确指定主题的项
|
||||||
|
globalThemes: [
|
||||||
|
{ bg: 'linear-gradient(135deg, #FFF7ED, #FFEDD5)', color: '#F97316' }, // 橙金
|
||||||
|
{ bg: 'linear-gradient(135deg, #EFF6FF, #DBEAFE)', color: '#3B82F6' }, // 科技蓝
|
||||||
|
{ bg: 'linear-gradient(135deg, #FAF5FF, #F3E8FF)', color: '#A855F7' }, // 优雅紫
|
||||||
|
{ bg: 'linear-gradient(135deg, #F0FDF4, #DCFCE7)', color: '#22C55E' }, // 安全绿
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
sections() {
|
||||||
|
const shared = '/subPackages/sub_business_shared'
|
||||||
|
const root = '/subPackages/sub_platform_admin'
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: 'finance',
|
||||||
|
title: '财务管理',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
key: 'withdrawal', label: '提现管理', desc: '提现流水查询', path: `${shared}/withdrawal/index`,
|
||||||
|
icon: 'red-packet-fill', // uView icon
|
||||||
|
theme: { bg: 'linear-gradient(135deg, #FFF7ED, #FFEDD5)', color: '#F97316' } // 橙金
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'withdrawal-audit', label: '提现审核', desc: '处理提现申请', path: `${root}/withdrawal/audit/index`,
|
||||||
|
icon: 'order',
|
||||||
|
theme: { bg: 'linear-gradient(135deg, #FEE2E2, #FECACA)', color: '#EF4444' } // 警戒红
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'reconciliation', label: '对账单', desc: '财务对账明细', path: `${shared}/reconciliation/index`,
|
||||||
|
icon: 'file-text-fill',
|
||||||
|
theme: { bg: 'linear-gradient(135deg, #EFF6FF, #DBEAFE)', color: '#3B82F6' } // 科技蓝
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'business',
|
||||||
|
title: '业务管理',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
key: 'order', label: '商品订单', desc: '全平台订单概览', path: `${shared}/order/index`,
|
||||||
|
icon: 'bag-fill',
|
||||||
|
theme: { bg: 'linear-gradient(135deg, #F0FDF4, #DCFCE7)', color: '#22C55E' } // 安全绿
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'warehouse',
|
||||||
|
title: '仓库管理',
|
||||||
|
// 动态加载的仓库数据,在 onShow 中会进行加工
|
||||||
|
items: this.warehouseMenus.length ? this.warehouseMenus : [
|
||||||
|
// 提供一个默认的占位符,防止异步加载完成前显示空白
|
||||||
|
{
|
||||||
|
key: 'warehouse-loading', label: '总仓库', desc: '库存管理中心', path: '/subPackages/sub_business_shared/warehouse/index',
|
||||||
|
icon: 'car-fill',
|
||||||
|
theme: { bg: 'linear-gradient(135deg, #F3F4F6, #E5E7EB)', color: '#6B7280' } // 占位灰
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'input-audit',
|
||||||
|
title: '信息审核',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
key: 'store-audit', label: '门店审核', desc: '新入门店审批', path: `${root}/store-input/audit/index`,
|
||||||
|
icon: 'home-fill',
|
||||||
|
theme: { bg: 'linear-gradient(135deg, #FAF5FF, #F3E8FF)', color: '#A855F7' } // 优雅紫
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'doctor-audit', label: '医生审核', desc: '入驻医生资质', path: `${root}/doctor-input/audit/index`,
|
||||||
|
icon: 'account-fill',
|
||||||
|
theme: { bg: 'linear-gradient(135deg, #EFF6FF, #DBEAFE)', color: '#3B82F6' } // 科技蓝
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'input-create',
|
||||||
|
title: '信息录入',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
key: 'store-list', label: '门店列表', desc: '平台门店档案', path: `${SALESPERSON_ROOT}/store-input/index`,
|
||||||
|
icon: 'grid-fill',
|
||||||
|
theme: { bg: 'linear-gradient(135deg, #ECFEFF, #CFFAFE)', color: '#06B6D4' } // 青色
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'store-form', label: '门店录入', desc: '新增线下门店', path: `${SALESPERSON_ROOT}/store-input/form`, inputType: 'store', isForm: true,
|
||||||
|
icon: 'plus-square-fill',
|
||||||
|
theme: { bg: 'linear-gradient(135deg, #F0FDF4, #DCFCE7)', color: '#22C55E' } // 绿色
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'doctor-list', label: '医生列表', desc: '平台医生名册', path: `${SALESPERSON_ROOT}/doctor-input/index`,
|
||||||
|
icon: 'list-dot',
|
||||||
|
theme: { bg: 'linear-gradient(135deg, #FFFBEB, #FEF3C7)', color: '#D97706' } // 琥珀色
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'doctor-form', label: '医生录入', desc: '新增合作医生', path: `${SALESPERSON_ROOT}/doctor-input/form`, inputType: 'doctor', isForm: true,
|
||||||
|
icon: 'plus-circle-fill',
|
||||||
|
theme: { bg: 'linear-gradient(135deg, #FFF7ED, #FFEDD5)', color: '#F97316' } // 橙金
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onShow() {
|
||||||
|
const user = uni.getStorageSync('platform_admin_user') || {}
|
||||||
|
this.displayName = user.nick_name || '平台管理员'
|
||||||
|
|
||||||
|
// 加载仓库数据
|
||||||
|
platformAdminApi.getWarehouseProductTypeOptions().then(w => {
|
||||||
|
const options = Array.isArray(w.data) ? w.data : []
|
||||||
|
if (!options.length) return
|
||||||
|
|
||||||
|
this.warehouseMenus = options.map((opt, index) => {
|
||||||
|
const value = opt.value != null ? opt.value : opt.id
|
||||||
|
const label = opt.label || opt.name || '仓库'
|
||||||
|
// 为动态仓库分配不同的主题色
|
||||||
|
const theme = this.globalThemes[index % this.globalThemes.length]
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: 'warehouse-' + value,
|
||||||
|
label,
|
||||||
|
desc: `${label}库存管理`, // 增强描述
|
||||||
|
icon: 'car-fill', // 仓库类目统一使用 uView 的车/物流图标
|
||||||
|
theme: theme,
|
||||||
|
path: `/subPackages/sub_business_shared/warehouse/index?type=${encodeURIComponent(value)}&label=${encodeURIComponent(label)}`,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}).catch(() => {})
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
go(item) {
|
||||||
|
if (item.isForm && item.inputType) {
|
||||||
|
navigateToInputForm(item.path, item.inputType)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.navigateTo({ url: item.path })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
/* 全局容器背景:更干净的高级浅灰底色 */
|
||||||
|
.workspace-container {
|
||||||
|
padding-bottom: 80rpx;
|
||||||
|
background-color: #F4F6F8;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================= 极简顶部英雄区 ================= */
|
||||||
|
.hero-section {
|
||||||
|
background: linear-gradient(135deg, #6acdbb 0%, #4db6a8 100%);
|
||||||
|
/* 调整内边距,移除原先负边距需要的大底部空间,因为这里不需要悬浮核心资产卡片 */
|
||||||
|
padding: 40rpx 40rpx 60rpx;
|
||||||
|
border-bottom-left-radius: 48rpx;
|
||||||
|
border-bottom-right-radius: 48rpx;
|
||||||
|
box-shadow: 0 4rpx 20rpx rgba(77, 182, 168, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-info-wrap {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-text {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
display: inline-flex;
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
padding: 6rpx 20rpx;
|
||||||
|
border-radius: 30rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-text {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 2rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-name {
|
||||||
|
font-size: 42rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-sub {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================= 主体内容区:快捷服务 ================= */
|
||||||
|
.content-wrapper {
|
||||||
|
margin-top: 30rpx; /* 轻微向上偏移,与顶部区域产生连接感 */
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-panel {
|
||||||
|
margin: 0 30rpx 30rpx; /* 增加底部间距 */
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
padding: 0 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-title {
|
||||||
|
font-size: 34rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1E293B; /* 深石板灰 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 采用与之前页面一致的宫格卡片布局 */
|
||||||
|
.service-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-card {
|
||||||
|
background: #FFFFFF;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
padding: 30rpx 24rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.02);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-card-hover {
|
||||||
|
transform: translateY(-2rpx);
|
||||||
|
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-icon-box {
|
||||||
|
width: 76rpx;
|
||||||
|
height: 76rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
box-shadow: inset 0 2rpx 4rpx rgba(255, 255, 255, 0.5); /* 顶部微高光,增加拟物质感 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-name {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1E293B;
|
||||||
|
margin-bottom: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-desc {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #94A3B8; /* 银灰色 */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
350
subPackages/sub_platform_admin/input/InputAuditSheet.vue
Normal file
350
subPackages/sub_platform_admin/input/InputAuditSheet.vue
Normal file
@@ -0,0 +1,350 @@
|
|||||||
|
<template>
|
||||||
|
<u-popup v-model="visible" mode="bottom" border-radius="24" :safe-area-inset-bottom="true">
|
||||||
|
<view class="sheet">
|
||||||
|
<view class="sheet-header">
|
||||||
|
<text class="sheet-title">{{ sheetTitle }}</text>
|
||||||
|
<text class="sheet-close" @click="close">关闭</text>
|
||||||
|
</view>
|
||||||
|
<scroll-view scroll-y class="sheet-scroll">
|
||||||
|
<view class="field-block">
|
||||||
|
<text class="field-label">审核结果</text>
|
||||||
|
<view class="radio-row">
|
||||||
|
<view
|
||||||
|
class="radio-item"
|
||||||
|
:class="{ active: auditStatus === 1 }"
|
||||||
|
@click="auditStatus = 1"
|
||||||
|
>审核通过</view>
|
||||||
|
<view
|
||||||
|
class="radio-item reject"
|
||||||
|
:class="{ active: auditStatus === 2 }"
|
||||||
|
@click="auditStatus = 2"
|
||||||
|
>审核拒绝</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<template v-if="inputType === 'store' && auditStatus === 1">
|
||||||
|
<view class="field-block">
|
||||||
|
<text class="field-label required">ERP ID</text>
|
||||||
|
<input v-model="erpId" class="field-input" placeholder="审核通过时必填" />
|
||||||
|
</view>
|
||||||
|
<view class="field-block">
|
||||||
|
<text class="field-label">MES ID</text>
|
||||||
|
<input v-model="mesId" class="field-input" placeholder="可选,煎药中心编码" />
|
||||||
|
</view>
|
||||||
|
<view class="field-block">
|
||||||
|
<view class="field-label-row">
|
||||||
|
<text class="field-label">一并审核关联医生(可选)</text>
|
||||||
|
<view v-if="pendingDoctorIds.length" class="link-actions">
|
||||||
|
<text class="link-btn" @click="selectAllPending">全选</text>
|
||||||
|
<text class="link-btn" @click="clearDoctors">取消全选</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="!allDoctors.length" class="hint">暂无关联医生预填</view>
|
||||||
|
<view v-else class="doctor-check-list">
|
||||||
|
<view
|
||||||
|
v-for="doc in allDoctors"
|
||||||
|
:key="doc.id"
|
||||||
|
class="doctor-check-item"
|
||||||
|
:class="{ disabled: !isDoctorPending(doc) }"
|
||||||
|
@click="toggleDoctor(doc)"
|
||||||
|
>
|
||||||
|
<view class="check-box" :class="{ checked: isDoctorSelected(doc.id), disabled: !isDoctorPending(doc) }">
|
||||||
|
<text v-if="isDoctorSelected(doc.id)">✓</text>
|
||||||
|
</view>
|
||||||
|
<view class="doctor-check-body">
|
||||||
|
<text class="doctor-check-name">{{ doc.name || '-' }}</text>
|
||||||
|
<text class="doctor-check-meta">{{ doctorMetaText(doc) }}</text>
|
||||||
|
<text class="doctor-check-status">{{ doctorStatusText(doc.status) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<view class="field-block">
|
||||||
|
<text class="field-label">审核备注</text>
|
||||||
|
<textarea
|
||||||
|
v-model="auditRemark"
|
||||||
|
class="field-textarea"
|
||||||
|
:placeholder="auditStatus === 2 ? '请输入拒绝原因(可选)' : '请输入审核备注(可选)'"
|
||||||
|
maxlength="500"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
<view class="sheet-footer">
|
||||||
|
<button class="footer-btn cancel" @click="close">取消</button>
|
||||||
|
<button class="footer-btn confirm" :loading="submitting" @click="submit">确认审核</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</u-popup>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { inputStatusText } from '@/subPackages/sub_salesperson/common/inputDetailFields.js'
|
||||||
|
|
||||||
|
function isDoctorPending(doc) {
|
||||||
|
return Number(doc?.status) === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'InputAuditSheet',
|
||||||
|
props: {
|
||||||
|
value: { type: Boolean, default: false },
|
||||||
|
inputType: { type: String, default: 'store' },
|
||||||
|
recordId: { type: Number, default: 0 },
|
||||||
|
info: { type: Object, default: () => ({}) },
|
||||||
|
defaultStatus: { type: Number, default: 1 },
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
auditStatus: 1,
|
||||||
|
auditRemark: '',
|
||||||
|
erpId: '',
|
||||||
|
mesId: '',
|
||||||
|
selectedDoctorIds: [],
|
||||||
|
submitting: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
visible: {
|
||||||
|
get() { return this.value },
|
||||||
|
set(v) { this.$emit('input', v) },
|
||||||
|
},
|
||||||
|
sheetTitle() {
|
||||||
|
return this.inputType === 'doctor' ? '审核医生预填' : '审核录入信息'
|
||||||
|
},
|
||||||
|
allDoctors() {
|
||||||
|
const list = this.info?.doctor_inputs ?? this.info?.doctorInputs ?? []
|
||||||
|
return Array.isArray(list) ? list : []
|
||||||
|
},
|
||||||
|
pendingDoctorIds() {
|
||||||
|
return this.allDoctors
|
||||||
|
.filter(isDoctorPending)
|
||||||
|
.map((d) => Number(d.id))
|
||||||
|
.filter((id) => id > 0)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
value(open) {
|
||||||
|
if (open) this.resetForm()
|
||||||
|
},
|
||||||
|
auditStatus(v) {
|
||||||
|
if (v !== 1) {
|
||||||
|
this.selectedDoctorIds = []
|
||||||
|
} else if (this.pendingDoctorIds.length) {
|
||||||
|
this.selectedDoctorIds = [...this.pendingDoctorIds]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
isDoctorPending,
|
||||||
|
resetForm() {
|
||||||
|
this.auditStatus = this.defaultStatus === 2 ? 2 : 1
|
||||||
|
this.auditRemark = ''
|
||||||
|
this.erpId = this.info?.erp_id != null ? String(this.info.erp_id) : ''
|
||||||
|
this.mesId = this.info?.mes_id != null ? String(this.info.mes_id) : ''
|
||||||
|
this.selectedDoctorIds = this.pendingDoctorIds.length ? [...this.pendingDoctorIds] : []
|
||||||
|
this.submitting = false
|
||||||
|
},
|
||||||
|
close() {
|
||||||
|
this.visible = false
|
||||||
|
},
|
||||||
|
selectAllPending() {
|
||||||
|
this.selectedDoctorIds = [...this.pendingDoctorIds]
|
||||||
|
},
|
||||||
|
clearDoctors() {
|
||||||
|
this.selectedDoctorIds = []
|
||||||
|
},
|
||||||
|
isDoctorSelected(id) {
|
||||||
|
return this.selectedDoctorIds.includes(Number(id))
|
||||||
|
},
|
||||||
|
toggleDoctor(doc) {
|
||||||
|
if (!isDoctorPending(doc)) return
|
||||||
|
const id = Number(doc.id)
|
||||||
|
if (!id) return
|
||||||
|
const idx = this.selectedDoctorIds.indexOf(id)
|
||||||
|
if (idx >= 0) this.selectedDoctorIds.splice(idx, 1)
|
||||||
|
else this.selectedDoctorIds.push(id)
|
||||||
|
},
|
||||||
|
doctorMetaText(doc) {
|
||||||
|
const depart = doc.depart?.name || ''
|
||||||
|
const title = doc.titleInfo?.name || doc.title_info?.name || ''
|
||||||
|
return [depart, title].filter(Boolean).join(' · ') || '-'
|
||||||
|
},
|
||||||
|
doctorStatusText(status) {
|
||||||
|
return inputStatusText(status)
|
||||||
|
},
|
||||||
|
buildPayload() {
|
||||||
|
const payload = {
|
||||||
|
id: this.recordId,
|
||||||
|
status: this.auditStatus,
|
||||||
|
audit_remark: this.auditRemark.trim(),
|
||||||
|
}
|
||||||
|
if (this.inputType === 'store' && this.auditStatus === 1) {
|
||||||
|
payload.erp_id = this.erpId.trim()
|
||||||
|
if (this.mesId.trim()) payload.mes_id = this.mesId.trim()
|
||||||
|
if (this.selectedDoctorIds.length) payload.doctor_input_ids = [...this.selectedDoctorIds]
|
||||||
|
}
|
||||||
|
return payload
|
||||||
|
},
|
||||||
|
validate() {
|
||||||
|
if (this.inputType === 'store' && this.auditStatus === 1 && !this.erpId.trim()) {
|
||||||
|
uni.showToast({ title: '审核通过时请填写 ERP ID', icon: 'none' })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
submit() {
|
||||||
|
if (!this.validate()) return
|
||||||
|
this.submitting = true
|
||||||
|
this.$emit('confirm', this.buildPayload(), () => {
|
||||||
|
this.submitting = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.sheet {
|
||||||
|
background: #fff;
|
||||||
|
max-height: 85vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.sheet-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 28rpx 32rpx;
|
||||||
|
border-bottom: 1rpx solid #f0f0f0;
|
||||||
|
}
|
||||||
|
.sheet-title { font-size: 32rpx; font-weight: 600; color: #1d2129; }
|
||||||
|
.sheet-close { font-size: 28rpx; color: #6acdbb; }
|
||||||
|
.sheet-scroll {
|
||||||
|
max-height: 60vh;
|
||||||
|
padding: 16rpx 32rpx 24rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.field-block { margin-bottom: 28rpx; }
|
||||||
|
.field-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #1d2129;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
.field-label.required::after { content: ' *'; color: #f53f3f; }
|
||||||
|
.field-label-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
.link-actions { display: flex; gap: 16rpx; }
|
||||||
|
.link-btn { font-size: 24rpx; color: #6acdbb; }
|
||||||
|
.field-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 72rpx;
|
||||||
|
padding: 0 20rpx;
|
||||||
|
border: 1rpx solid #e5e6eb;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.field-textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 160rpx;
|
||||||
|
padding: 16rpx 20rpx;
|
||||||
|
border: 1rpx solid #e5e6eb;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.radio-row { display: flex; gap: 16rpx; }
|
||||||
|
.radio-item {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
padding: 20rpx 0;
|
||||||
|
border: 1rpx solid #e5e6eb;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #4e5969;
|
||||||
|
}
|
||||||
|
.radio-item.active {
|
||||||
|
border-color: #6acdbb;
|
||||||
|
color: #6acdbb;
|
||||||
|
background: #f0faf8;
|
||||||
|
}
|
||||||
|
.radio-item.reject.active {
|
||||||
|
border-color: #f53f3f;
|
||||||
|
color: #f53f3f;
|
||||||
|
background: #fff2f0;
|
||||||
|
}
|
||||||
|
.hint { font-size: 26rpx; color: #86909c; }
|
||||||
|
.doctor-check-list { display: flex; flex-direction: column; gap: 12rpx; }
|
||||||
|
.doctor-check-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 16rpx;
|
||||||
|
background: #f7f8fa;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
}
|
||||||
|
.doctor-check-item.disabled { opacity: 0.55; }
|
||||||
|
.check-box {
|
||||||
|
width: 36rpx;
|
||||||
|
height: 36rpx;
|
||||||
|
border: 2rpx solid #c9cdd4;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
margin-right: 16rpx;
|
||||||
|
margin-top: 6rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #fff;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.check-box.checked { background: #6acdbb; border-color: #6acdbb; }
|
||||||
|
.check-box.disabled { background: #f2f3f5; }
|
||||||
|
.doctor-check-body { flex: 1; min-width: 0; }
|
||||||
|
.doctor-check-name {
|
||||||
|
display: block;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1d2129;
|
||||||
|
}
|
||||||
|
.doctor-check-meta {
|
||||||
|
display: block;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #86909c;
|
||||||
|
margin-top: 6rpx;
|
||||||
|
}
|
||||||
|
.doctor-check-status {
|
||||||
|
display: block;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #6acdbb;
|
||||||
|
margin-top: 4rpx;
|
||||||
|
}
|
||||||
|
.sheet-footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 16rpx;
|
||||||
|
padding: 20rpx 32rpx calc(20rpx + env(safe-area-inset-bottom));
|
||||||
|
border-top: 1rpx solid #f0f0f0;
|
||||||
|
}
|
||||||
|
.footer-btn {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 28rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
line-height: 80rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
}
|
||||||
|
.footer-btn.cancel {
|
||||||
|
background: #f2f3f5;
|
||||||
|
color: #4e5969;
|
||||||
|
}
|
||||||
|
.footer-btn.confirm {
|
||||||
|
background: #6acdbb;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
363
subPackages/sub_platform_admin/input/InputDetailPage.vue
Normal file
363
subPackages/sub_platform_admin/input/InputDetailPage.vue
Normal file
@@ -0,0 +1,363 @@
|
|||||||
|
<template>
|
||||||
|
<business-page-layout :title="pageTitle" :show-back="true">
|
||||||
|
<view v-if="loading" class="loading-wrap">加载中...</view>
|
||||||
|
<view v-else-if="!info.id" class="loading-wrap">暂无数据</view>
|
||||||
|
<view v-else class="detail-page">
|
||||||
|
<scroll-view scroll-y class="detail-scroll">
|
||||||
|
<view class="detail-wrap">
|
||||||
|
<view v-for="section in sections" :key="section.title" class="section-block">
|
||||||
|
<view class="section-title">{{ section.title }}</view>
|
||||||
|
<template v-for="field in section.fields">
|
||||||
|
<view
|
||||||
|
v-if="shouldShowField(field)"
|
||||||
|
:key="field.key"
|
||||||
|
class="row"
|
||||||
|
:class="{ multiline: field.multiline }"
|
||||||
|
>
|
||||||
|
<text class="k">{{ field.label }}</text>
|
||||||
|
<view class="v">
|
||||||
|
<view v-if="field.type === 'image' && getImages(field).length" class="img-list">
|
||||||
|
<image
|
||||||
|
v-for="(url, idx) in getImages(field)"
|
||||||
|
:key="idx"
|
||||||
|
:src="url"
|
||||||
|
class="thumb"
|
||||||
|
mode="aspectFill"
|
||||||
|
@click="previewImages(getImages(field), idx)"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view v-else-if="field.type === 'images' && getImages(field).length" class="img-list">
|
||||||
|
<image
|
||||||
|
v-for="(url, idx) in getImages(field)"
|
||||||
|
:key="idx"
|
||||||
|
:src="url"
|
||||||
|
class="thumb"
|
||||||
|
mode="aspectFill"
|
||||||
|
@click="previewImages(getImages(field), idx)"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view v-else-if="field.type === 'files' && getFiles(field).length" class="file-list">
|
||||||
|
<text
|
||||||
|
v-for="(file, idx) in getFiles(field)"
|
||||||
|
:key="idx"
|
||||||
|
class="file-link"
|
||||||
|
@click="openFile(file.url)"
|
||||||
|
>{{ file.name }}</text>
|
||||||
|
</view>
|
||||||
|
<text v-else class="v-text">{{ fieldValue(field) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
</view>
|
||||||
|
<view v-if="showLinkedDoctors" class="section-block linked-doctors-block">
|
||||||
|
<view class="section-title">关联医生</view>
|
||||||
|
<view v-if="!linkedDoctors.length" class="linked-empty">暂无关联医生预填</view>
|
||||||
|
<view
|
||||||
|
v-for="doc in linkedDoctors"
|
||||||
|
:key="doc.id"
|
||||||
|
class="doctor-card"
|
||||||
|
@click="goDoctorDetail(doc.id)"
|
||||||
|
>
|
||||||
|
<image
|
||||||
|
v-if="doc.avatar"
|
||||||
|
:src="doc.avatar"
|
||||||
|
class="doctor-avatar"
|
||||||
|
mode="aspectFill"
|
||||||
|
/>
|
||||||
|
<view v-else class="doctor-avatar doctor-avatar-placeholder">
|
||||||
|
<text>{{ (doc.name || '?').charAt(0) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="doctor-card-body">
|
||||||
|
<text class="doctor-name">{{ doc.name || '-' }}</text>
|
||||||
|
<text class="doctor-meta">{{ doctorMetaText(doc) }}</text>
|
||||||
|
<text class="doctor-status">{{ doctorStatusText(doc.status) }}</text>
|
||||||
|
</view>
|
||||||
|
<u-icon name="arrow-right" color="#c9cdd4" size="28" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
<view v-if="canAudit" class="footer-actions">
|
||||||
|
<u-button class="action-btn reject" @click="openAudit(2)">审核拒绝</u-button>
|
||||||
|
<u-button class="action-btn" type="primary" :custom-style="primaryStyle" @click="openAudit(1)">审核通过</u-button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<input-audit-sheet
|
||||||
|
v-model="showAuditSheet"
|
||||||
|
:input-type="inputType"
|
||||||
|
:record-id="recordId"
|
||||||
|
:info="info"
|
||||||
|
:default-status="auditDefaultStatus"
|
||||||
|
@confirm="onAuditConfirm"
|
||||||
|
/>
|
||||||
|
</business-page-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import BusinessPageLayout from '@/subPackages/sub_platform_admin/components/business-page-layout.vue'
|
||||||
|
import InputAuditSheet from '@/subPackages/sub_platform_admin/input/InputAuditSheet.vue'
|
||||||
|
import businessMixin from '@/subPackages/sub_platform_admin/common/businessMixin.js'
|
||||||
|
import {
|
||||||
|
getStoreDetailSections,
|
||||||
|
getDoctorDetailSections,
|
||||||
|
formatDetailFieldValue,
|
||||||
|
getDetailImageUrls,
|
||||||
|
getDetailFileLinks,
|
||||||
|
getLinkedDoctorInputs,
|
||||||
|
inputStatusText,
|
||||||
|
} from '@/subPackages/sub_salesperson/common/inputDetailFields.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [businessMixin],
|
||||||
|
components: { BusinessPageLayout, InputAuditSheet },
|
||||||
|
props: {
|
||||||
|
recordId: { type: Number, default: 0 },
|
||||||
|
inputType: { type: String, default: 'store' },
|
||||||
|
doctorDetailPath: {
|
||||||
|
type: String,
|
||||||
|
default: '/subPackages/sub_platform_admin/doctor-input/audit/detail',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
info: {},
|
||||||
|
loading: false,
|
||||||
|
showAuditSheet: false,
|
||||||
|
auditDefaultStatus: 1,
|
||||||
|
primaryStyle: {
|
||||||
|
backgroundColor: '#6acdbb',
|
||||||
|
color: '#ffffff',
|
||||||
|
borderColor: '#6acdbb',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
pageTitle() {
|
||||||
|
if (this.inputType === 'doctor') return '医生预填详情'
|
||||||
|
return '门店录入详情'
|
||||||
|
},
|
||||||
|
sections() {
|
||||||
|
return this.inputType === 'doctor' ? getDoctorDetailSections() : getStoreDetailSections()
|
||||||
|
},
|
||||||
|
canAudit() {
|
||||||
|
if (Number(this.info.status) !== 0) return false
|
||||||
|
if (this.inputType === 'doctor') return !!this.bizCap.canDoctorInputAudit
|
||||||
|
return !!this.bizCap.canStoreInputAudit
|
||||||
|
},
|
||||||
|
showLinkedDoctors() {
|
||||||
|
return this.inputType === 'store' && !!this.info.id
|
||||||
|
},
|
||||||
|
linkedDoctors() {
|
||||||
|
return getLinkedDoctorInputs(this.info)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
recordId: {
|
||||||
|
immediate: true,
|
||||||
|
handler(id) {
|
||||||
|
if (Number(id) > 0) this.load()
|
||||||
|
else {
|
||||||
|
this.info = {}
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
detailApi() {
|
||||||
|
return this.inputType === 'doctor'
|
||||||
|
? this.bizApi.getDoctorInputDetail(this.recordId)
|
||||||
|
: this.bizApi.getStoreInputDetail(this.recordId)
|
||||||
|
},
|
||||||
|
auditApi(data) {
|
||||||
|
return this.inputType === 'doctor'
|
||||||
|
? this.bizApi.auditDoctorInput(data)
|
||||||
|
: this.bizApi.auditStoreInput(data)
|
||||||
|
},
|
||||||
|
load() {
|
||||||
|
if (!this.recordId) return
|
||||||
|
this.loading = true
|
||||||
|
this.info = {}
|
||||||
|
this.detailApi()
|
||||||
|
.then((w) => {
|
||||||
|
if (w.ok) this.info = w.data || {}
|
||||||
|
else uni.showToast({ title: (w.res && (w.res.message || w.res.msg)) || '加载失败', icon: 'none' })
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
shouldShowField(field) {
|
||||||
|
if (field.show && !field.show(this.info)) return false
|
||||||
|
if (field.type === 'image' || field.type === 'images') {
|
||||||
|
return getDetailImageUrls(field, this.info).length > 0
|
||||||
|
}
|
||||||
|
if (field.type === 'files') {
|
||||||
|
return getDetailFileLinks(field, this.info).length > 0
|
||||||
|
}
|
||||||
|
const val = formatDetailFieldValue(field, this.info)
|
||||||
|
return val != null && val !== ''
|
||||||
|
},
|
||||||
|
fieldValue(field) {
|
||||||
|
return formatDetailFieldValue(field, this.info) ?? '-'
|
||||||
|
},
|
||||||
|
getImages(field) {
|
||||||
|
return getDetailImageUrls(field, this.info)
|
||||||
|
},
|
||||||
|
getFiles(field) {
|
||||||
|
return getDetailFileLinks(field, this.info)
|
||||||
|
},
|
||||||
|
previewImages(urls, index) {
|
||||||
|
uni.previewImage({ urls, current: urls[index] })
|
||||||
|
},
|
||||||
|
openFile(url) {
|
||||||
|
if (!url) return
|
||||||
|
uni.setClipboardData({
|
||||||
|
data: url,
|
||||||
|
success: () => uni.showToast({ title: '链接已复制', icon: 'none' }),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
goDoctorDetail(doctorId) {
|
||||||
|
if (!doctorId || !this.doctorDetailPath) return
|
||||||
|
uni.navigateTo({ url: `${this.doctorDetailPath}?id=${doctorId}` })
|
||||||
|
},
|
||||||
|
doctorMetaText(doc) {
|
||||||
|
const depart = doc.depart?.name || ''
|
||||||
|
const title = doc.titleInfo?.name || doc.title_info?.name || ''
|
||||||
|
return [depart, title].filter(Boolean).join(' · ') || '-'
|
||||||
|
},
|
||||||
|
doctorStatusText(status) {
|
||||||
|
return inputStatusText(status)
|
||||||
|
},
|
||||||
|
openAudit(status) {
|
||||||
|
this.auditDefaultStatus = status
|
||||||
|
this.showAuditSheet = true
|
||||||
|
},
|
||||||
|
onAuditConfirm(payload, done) {
|
||||||
|
this.auditApi(payload)
|
||||||
|
.then((w) => {
|
||||||
|
if (w.ok) {
|
||||||
|
const doctorCount = payload.doctor_input_ids?.length || 0
|
||||||
|
const msg = doctorCount > 0
|
||||||
|
? `审核成功,已一并通过 ${doctorCount} 位医生预填`
|
||||||
|
: '操作成功'
|
||||||
|
uni.showToast({ title: msg })
|
||||||
|
this.showAuditSheet = false
|
||||||
|
setTimeout(() => uni.navigateBack(), 500)
|
||||||
|
} else {
|
||||||
|
uni.showToast({
|
||||||
|
title: (w.res && (w.res.message || w.res.msg)) || w.msg || '操作失败',
|
||||||
|
icon: 'none',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => uni.showToast({ title: '操作失败', icon: 'none' }))
|
||||||
|
.finally(() => {
|
||||||
|
if (typeof done === 'function') done()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.loading-wrap { text-align: center; color: #86909c; padding: 120rpx 0; font-size: 28rpx; }
|
||||||
|
.detail-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: calc(100vh - 200rpx);
|
||||||
|
min-height: 400rpx;
|
||||||
|
}
|
||||||
|
.detail-scroll {
|
||||||
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.detail-wrap { padding: 24rpx; padding-bottom: 32rpx; }
|
||||||
|
.section-block {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
padding: 0 28rpx 8rpx;
|
||||||
|
}
|
||||||
|
.section-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1d2129;
|
||||||
|
padding: 28rpx 0 16rpx;
|
||||||
|
border-bottom: 1rpx solid #f2f3f5;
|
||||||
|
}
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
padding: 24rpx 0;
|
||||||
|
border-bottom: 1rpx solid #f2f3f5;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.row:last-child { border-bottom: none; }
|
||||||
|
.row.multiline .v { line-height: 1.6; }
|
||||||
|
.k { width: 200rpx; color: #86909c; font-size: 28rpx; flex-shrink: 0; }
|
||||||
|
.v { flex: 1; min-width: 0; }
|
||||||
|
.v-text { font-size: 28rpx; color: #1d2129; word-break: break-all; }
|
||||||
|
.img-list { display: flex; flex-wrap: wrap; gap: 16rpx; }
|
||||||
|
.thumb { width: 160rpx; height: 160rpx; border-radius: 12rpx; }
|
||||||
|
.file-list { display: flex; flex-direction: column; gap: 12rpx; }
|
||||||
|
.file-link { font-size: 28rpx; color: #6acdbb; word-break: break-all; }
|
||||||
|
.footer-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 16rpx;
|
||||||
|
padding: 24rpx;
|
||||||
|
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
|
||||||
|
background: #f5f7f8;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.action-btn { flex: 1; min-width: 200rpx; }
|
||||||
|
.action-btn.reject { color: #f53f3f; border-color: #f53f3f; }
|
||||||
|
.linked-empty {
|
||||||
|
padding: 32rpx 0 40rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #86909c;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.doctor-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 24rpx 0;
|
||||||
|
border-bottom: 1rpx solid #f2f3f5;
|
||||||
|
}
|
||||||
|
.doctor-card:last-child { border-bottom: none; }
|
||||||
|
.doctor-avatar {
|
||||||
|
width: 88rpx;
|
||||||
|
height: 88rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
background: #f2f3f5;
|
||||||
|
}
|
||||||
|
.doctor-avatar-placeholder {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #86909c;
|
||||||
|
}
|
||||||
|
.doctor-card-body { flex: 1; min-width: 0; }
|
||||||
|
.doctor-name {
|
||||||
|
display: block;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1d2129;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
.doctor-meta {
|
||||||
|
display: block;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #86909c;
|
||||||
|
margin-bottom: 6rpx;
|
||||||
|
}
|
||||||
|
.doctor-status { font-size: 24rpx; color: #6acdbb; }
|
||||||
|
</style>
|
||||||
110
subPackages/sub_platform_admin/input/InputListPage.vue
Normal file
110
subPackages/sub_platform_admin/input/InputListPage.vue
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
<template>
|
||||||
|
<business-page-layout :title="title" :show-back="true">
|
||||||
|
<view class="page-wrap">
|
||||||
|
<view v-if="!auditMode" class="toolbar">
|
||||||
|
<button class="btn" @click="goForm()">新建录入</button>
|
||||||
|
</view>
|
||||||
|
<view v-for="item in list" :key="item.id" class="card" @click="goDetail(item)">
|
||||||
|
<view class="row">
|
||||||
|
<text class="name">{{ item.name }}</text>
|
||||||
|
<text class="status" :class="'s' + item.status">{{ statusText(item.status) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="sub">{{ item.mobile }} · {{ displayTime(item.created_at) }}</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="!list.length" class="empty">暂无记录</view>
|
||||||
|
</view>
|
||||||
|
</business-page-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import BusinessPageLayout from '@/subPackages/sub_platform_admin/components/business-page-layout.vue'
|
||||||
|
import businessMixin from '@/subPackages/sub_platform_admin/common/businessMixin.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [businessMixin],
|
||||||
|
components: { BusinessPageLayout },
|
||||||
|
props: {
|
||||||
|
auditMode: { type: Boolean, default: false },
|
||||||
|
inputType: { type: String, default: 'store' },
|
||||||
|
formPath: { type: String, default: '' },
|
||||||
|
detailPath: { type: String, required: true },
|
||||||
|
},
|
||||||
|
data() { return { list: [], page: 1 } },
|
||||||
|
computed: {
|
||||||
|
title() {
|
||||||
|
if (this.inputType === 'doctor') return this.auditMode ? '医生信息审核' : '医生信息预填'
|
||||||
|
return this.auditMode ? '诊所信息审核' : '诊所信息录入'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.reload()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
listApi(params) {
|
||||||
|
return this.inputType === 'doctor'
|
||||||
|
? this.bizApi.getDoctorInputList(params)
|
||||||
|
: this.bizApi.getStoreInputList(params)
|
||||||
|
},
|
||||||
|
parseListItems(data) {
|
||||||
|
if (Array.isArray(data)) return data
|
||||||
|
if (data && Array.isArray(data.items)) return data.items
|
||||||
|
if (data && Array.isArray(data.data)) return data.data
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
reload() { this.page = 1; this.load(1, false) },
|
||||||
|
load(page, append) {
|
||||||
|
const params = { page, pageSize: 15 }
|
||||||
|
if (this.auditMode) params.status = 0
|
||||||
|
this.listApi(params)
|
||||||
|
.then(w => {
|
||||||
|
const items = this.parseListItems(w.data)
|
||||||
|
if (!items.length && !w.ok) {
|
||||||
|
uni.showToast({ title: (w.res && (w.res.message || w.res.msg)) || '加载失败', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.list = append ? this.list.concat(items) : items
|
||||||
|
this.page = page
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
statusText(s) {
|
||||||
|
return { 0: '待审核', 1: '已通过', 2: '已拒绝' }[Number(s)] || '-'
|
||||||
|
},
|
||||||
|
displayTime(t) {
|
||||||
|
if (t == null || t === '') return '-'
|
||||||
|
if (typeof t === 'string' && (t.includes('-') || t.includes(':'))) {
|
||||||
|
return t.length >= 10 ? t.slice(0, 10) : t
|
||||||
|
}
|
||||||
|
const n = Number(t)
|
||||||
|
if (!Number.isFinite(n) || n <= 0) return '-'
|
||||||
|
const ms = n < 1e12 ? n * 1000 : n
|
||||||
|
const d = new Date(ms)
|
||||||
|
const pad = (x) => String(x).padStart(2, '0')
|
||||||
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
|
||||||
|
},
|
||||||
|
goForm() {
|
||||||
|
if (this.formPath) uni.navigateTo({ url: this.formPath })
|
||||||
|
},
|
||||||
|
goDetail(item) {
|
||||||
|
uni.navigateTo({ url: `${this.detailPath}?id=${item.id}` })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page-wrap { padding: 24rpx; }
|
||||||
|
.toolbar { margin-bottom: 24rpx; }
|
||||||
|
.btn { background: #6acdbb; color: #fff; font-size: 28rpx; }
|
||||||
|
.card { background: #fff; padding: 28rpx; border-radius: 16rpx; margin-bottom: 16rpx; }
|
||||||
|
.row { display: flex; justify-content: space-between; align-items: center; }
|
||||||
|
.name { font-size: 30rpx; font-weight: 600; }
|
||||||
|
.status { font-size: 24rpx; }
|
||||||
|
.s0 { color: #FF7D00; }
|
||||||
|
.s1 { color: #00B42A; }
|
||||||
|
.s2 { color: #F53F3F; }
|
||||||
|
.sub { margin-top: 12rpx; color: #86909C; font-size: 24rpx; }
|
||||||
|
.empty { text-align: center; color: #86909C; padding: 80rpx 0; }
|
||||||
|
</style>
|
||||||
65
subPackages/sub_platform_admin/my/index.vue
Normal file
65
subPackages/sub_platform_admin/my/index.vue
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<template>
|
||||||
|
<business-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">{{ roleText }}</text></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="setting-panel">
|
||||||
|
<account-switch-entry />
|
||||||
|
<view class="setting-item action-item" @click="logout">
|
||||||
|
<text class="danger-text">退出登录</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</business-page-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import BusinessPageLayout from '@/subPackages/sub_platform_admin/components/business-page-layout.vue'
|
||||||
|
import AccountSwitchEntry from '@/components/account-switch/account-switch-entry.vue'
|
||||||
|
import { getPlatformAdminContext } from '@/subPackages/sub_platform_admin/common/context.js'
|
||||||
|
import { clearLoginStorage } from '@/utils/loginSession.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { BusinessPageLayout, AccountSwitchEntry },
|
||||||
|
provide() { return { businessContext: getPlatformAdminContext() } },
|
||||||
|
data() { return { displayName: '', roleText: '平台管理员' } },
|
||||||
|
computed: {
|
||||||
|
avatarText() { return (this.displayName || '管').slice(0, 1) },
|
||||||
|
},
|
||||||
|
onShow() {
|
||||||
|
const user = uni.getStorageSync('platform_admin_user') || {}
|
||||||
|
this.displayName = user.nick_name || '管理员'
|
||||||
|
this.roleText = (user.roles && user.roles.name) || '平台管理员'
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
logout() {
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示', content: '确定退出登录?',
|
||||||
|
success: (res) => {
|
||||||
|
if (!res.confirm) return
|
||||||
|
clearLoginStorage()
|
||||||
|
uni.reLaunch({ url: '/pages/login/index' })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.mine-container { min-height: 100vh; background: #F5F7F8; }
|
||||||
|
.profile-panel { display: flex; align-items: center; background: #fff; padding: 60rpx 40rpx; margin-bottom: 24rpx; }
|
||||||
|
.avatar-squircle { width: 128rpx; height: 128rpx; border-radius: 40rpx; background: linear-gradient(135deg, #6acdbb, #4db6a8); color: #fff; font-size: 52rpx; display: flex; align-items: center; justify-content: center; }
|
||||||
|
.user-info { margin-left: 32rpx; }
|
||||||
|
.user-name { font-size: 40rpx; font-weight: 700; display: block; margin-bottom: 12rpx; }
|
||||||
|
.role-badge { display: inline-block; background: rgba(106,205,187,.1); padding: 4rpx 16rpx; border-radius: 8rpx; }
|
||||||
|
.role-text { font-size: 22rpx; color: #4db6a8; }
|
||||||
|
.setting-panel { background: #fff; padding: 0 40rpx; margin-bottom: 24rpx; }
|
||||||
|
.setting-item { padding: 36rpx 0; border-bottom: 1rpx solid #F2F3F5; }
|
||||||
|
.danger-text { color: #F53F3F; font-size: 30rpx; }
|
||||||
|
</style>
|
||||||
17
subPackages/sub_platform_admin/store-input/audit/detail.vue
Normal file
17
subPackages/sub_platform_admin/store-input/audit/detail.vue
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<template>
|
||||||
|
<input-detail-page :record-id="recordId" input-type="store" />
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import InputDetailPage from '@/subPackages/sub_platform_admin/input/InputDetailPage.vue'
|
||||||
|
import { getPlatformAdminContext } from '@/subPackages/sub_platform_admin/common/context.js'
|
||||||
|
export default {
|
||||||
|
components: { InputDetailPage },
|
||||||
|
provide() { return { businessContext: getPlatformAdminContext() } },
|
||||||
|
data() {
|
||||||
|
return { recordId: 0 }
|
||||||
|
},
|
||||||
|
onLoad(q) {
|
||||||
|
this.recordId = Number(q.id || 0)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
24
subPackages/sub_platform_admin/store-input/audit/index.vue
Normal file
24
subPackages/sub_platform_admin/store-input/audit/index.vue
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<template>
|
||||||
|
<input-list-page
|
||||||
|
ref="listPage"
|
||||||
|
:audit-mode="true"
|
||||||
|
input-type="store"
|
||||||
|
form-path=""
|
||||||
|
detail-path="/subPackages/sub_platform_admin/store-input/audit/detail"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import InputListPage from '@/subPackages/sub_platform_admin/input/InputListPage.vue'
|
||||||
|
import { getPlatformAdminContext } from '@/subPackages/sub_platform_admin/common/context.js'
|
||||||
|
export default {
|
||||||
|
components: { InputListPage },
|
||||||
|
provide() { return { businessContext: getPlatformAdminContext() } },
|
||||||
|
onShow() {
|
||||||
|
this.$nextTick(() => this.$refs.listPage?.reload?.())
|
||||||
|
},
|
||||||
|
onReachBottom() {
|
||||||
|
const p = this.$refs.listPage
|
||||||
|
if (p) p.load(p.page + 1, true)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
143
subPackages/sub_platform_admin/withdrawal/audit/index.vue
Normal file
143
subPackages/sub_platform_admin/withdrawal/audit/index.vue
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
<template>
|
||||||
|
<business-page-layout title="提现审核" :show-back="true">
|
||||||
|
<view class="page-wrap">
|
||||||
|
<view v-for="item in list" :key="item.id" class="card">
|
||||||
|
<view class="row">
|
||||||
|
<view class="title-wrap">
|
||||||
|
<text class="type-tag">{{ item.userTypeText }}</text>
|
||||||
|
<text class="name">{{ item.entityName }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="amount">{{ item.amountText }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="sub">订单号:{{ item.orderNo }}</view>
|
||||||
|
<view class="sub">申请时间:{{ item.applyTime }}</view>
|
||||||
|
<view class="status-row">
|
||||||
|
<text class="status" :class="'s' + item.check_status">{{ item.statusText }}</text>
|
||||||
|
</view>
|
||||||
|
<view v-if="item.check_status === 1" class="actions">
|
||||||
|
<button size="mini" class="ok" :disabled="submitting" @click="auditPass(item)">通过</button>
|
||||||
|
<button size="mini" class="no" :disabled="submitting" @click="auditRefuse(item)">拒绝</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="!list.length" class="empty">暂无待审核记录</view>
|
||||||
|
</view>
|
||||||
|
</business-page-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import BusinessPageLayout from '@/subPackages/sub_platform_admin/components/business-page-layout.vue'
|
||||||
|
import { getPlatformAdminContext } from '@/subPackages/sub_platform_admin/common/context.js'
|
||||||
|
import platformAdminApi from '@/api/platformAdmin.js'
|
||||||
|
import { formatMoney } from '@/subPackages/sub_business_shared/common/format.js'
|
||||||
|
import { formatDateTime, withdrawalCheckStatusText } from '@/subPackages/sub_business_shared/common/display.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { BusinessPageLayout },
|
||||||
|
provide() { return { businessContext: getPlatformAdminContext() } },
|
||||||
|
data() {
|
||||||
|
return { list: [], submitting: false }
|
||||||
|
},
|
||||||
|
onShow() { this.load() },
|
||||||
|
methods: {
|
||||||
|
mapItem(item) {
|
||||||
|
const store = item.store || {}
|
||||||
|
const supplier = item.supplier || {}
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
userTypeText: item.user_type_txt || '平台',
|
||||||
|
entityName: store.name || supplier.name || '萧康云医',
|
||||||
|
amountText: formatMoney(item.apply_cash),
|
||||||
|
orderNo: item.order_no || '-',
|
||||||
|
applyTime: formatDateTime(item.apply_time),
|
||||||
|
statusText: item.check_status_txt || withdrawalCheckStatusText(item.check_status),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
load() {
|
||||||
|
platformAdminApi.getWithdrawalAuditList({ page: 1, pageSize: 50, check_status: 1 }).then(w => {
|
||||||
|
const items = (w.data && w.data.items) || []
|
||||||
|
this.list = items.map(item => this.mapItem(item))
|
||||||
|
})
|
||||||
|
},
|
||||||
|
showError(w) {
|
||||||
|
const msg = (w.res && (w.res.message || w.res.msg)) || '操作失败'
|
||||||
|
uni.showToast({ title: msg, icon: 'none' })
|
||||||
|
},
|
||||||
|
submitAudit(item, api, payload) {
|
||||||
|
if (this.submitting) return
|
||||||
|
this.submitting = true
|
||||||
|
api(payload).then(w => {
|
||||||
|
if (w.ok) {
|
||||||
|
uni.showToast({ title: '操作成功' })
|
||||||
|
this.load()
|
||||||
|
} else {
|
||||||
|
this.showError(w)
|
||||||
|
}
|
||||||
|
}).finally(() => {
|
||||||
|
this.submitting = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
auditPass(item) {
|
||||||
|
uni.showModal({
|
||||||
|
title: '审核通过',
|
||||||
|
content: `确认通过 ${item.entityName} 的提现申请 ${item.amountText}?`,
|
||||||
|
success: (res) => {
|
||||||
|
if (!res.confirm) return
|
||||||
|
this.submitAudit(item, platformAdminApi.passWithdrawal, {
|
||||||
|
id: item.id,
|
||||||
|
reason: '审核通过',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
auditRefuse(item) {
|
||||||
|
uni.showModal({
|
||||||
|
title: '拒绝审核',
|
||||||
|
content: '请输入拒绝原因',
|
||||||
|
editable: true,
|
||||||
|
placeholderText: '拒绝原因',
|
||||||
|
success: (res) => {
|
||||||
|
if (!res.confirm) return
|
||||||
|
const reason = (res.content || '').trim()
|
||||||
|
if (!reason) {
|
||||||
|
uni.showToast({ title: '请输入拒绝原因', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.submitAudit(item, platformAdminApi.refuseWithdrawal, {
|
||||||
|
id: item.id,
|
||||||
|
reason,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page-wrap { padding: 24rpx; }
|
||||||
|
.card { background: #fff; padding: 28rpx; border-radius: 16rpx; margin-bottom: 16rpx; }
|
||||||
|
.row { display: flex; justify-content: space-between; align-items: flex-start; }
|
||||||
|
.title-wrap { flex: 1; min-width: 0; }
|
||||||
|
.type-tag {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 12rpx;
|
||||||
|
padding: 4rpx 12rpx;
|
||||||
|
background: #E8FFEA;
|
||||||
|
color: #00B42A;
|
||||||
|
font-size: 22rpx;
|
||||||
|
border-radius: 6rpx;
|
||||||
|
}
|
||||||
|
.name { font-size: 30rpx; font-weight: 600; }
|
||||||
|
.amount { font-size: 32rpx; font-weight: 600; color: #1D2129; flex-shrink: 0; margin-left: 16rpx; }
|
||||||
|
.sub { margin-top: 12rpx; color: #86909C; font-size: 24rpx; }
|
||||||
|
.status-row { margin-top: 12rpx; }
|
||||||
|
.status { font-size: 24rpx; }
|
||||||
|
.s1 { color: #FF7D00; }
|
||||||
|
.s2 { color: #00B42A; }
|
||||||
|
.s3 { color: #F53F3F; }
|
||||||
|
.s4 { color: #F53F3F; }
|
||||||
|
.actions { display: flex; gap: 16rpx; margin-top: 20rpx; }
|
||||||
|
.ok { background: #6acdbb; color: #fff; }
|
||||||
|
.no { background: #F53F3F; color: #fff; }
|
||||||
|
.empty { text-align: center; color: #86909C; padding: 80rpx 0; }
|
||||||
|
</style>
|
||||||
26
subPackages/sub_salesperson/common/businessMixin.js
Normal file
26
subPackages/sub_salesperson/common/businessMixin.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { resolveBusinessContext, pageUrl, businessPageRoot } from './context.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
inject: {
|
||||||
|
businessContext: { default: null },
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
bizCtx() {
|
||||||
|
return resolveBusinessContext(this.businessContext);
|
||||||
|
},
|
||||||
|
bizApi() {
|
||||||
|
return this.bizCtx.api;
|
||||||
|
},
|
||||||
|
bizCap() {
|
||||||
|
return this.bizCtx.capabilities || {};
|
||||||
|
},
|
||||||
|
bizRoot() {
|
||||||
|
return businessPageRoot(this.bizCtx);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
bizPage(subPath) {
|
||||||
|
return pageUrl(this.bizCtx, subPath);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
50
subPackages/sub_salesperson/common/context.js
Normal file
50
subPackages/sub_salesperson/common/context.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { salespersonApi } from '@/api/businessApi.js';
|
||||||
|
import { getPlatformAdminContext } from '@/subPackages/sub_business_shared/common/context.js';
|
||||||
|
|
||||||
|
const BUSINESS_PACKAGE_ROOT = '/subPackages/sub_business_shared';
|
||||||
|
const SALESPERSON_HOME = '/subPackages/sub_salesperson/home/index';
|
||||||
|
const SALESPERSON_MY = '/subPackages/sub_salesperson/my/index';
|
||||||
|
|
||||||
|
export function getSalespersonContext() {
|
||||||
|
return {
|
||||||
|
mode: 'salesperson',
|
||||||
|
api: salespersonApi,
|
||||||
|
userStorageKey: 'salesperson_user',
|
||||||
|
homePath: SALESPERSON_HOME,
|
||||||
|
myPath: SALESPERSON_MY,
|
||||||
|
packageRoot: '/subPackages/sub_salesperson',
|
||||||
|
businessPackageRoot: BUSINESS_PACKAGE_ROOT,
|
||||||
|
brandTitle: '业务工作台',
|
||||||
|
capabilities: {
|
||||||
|
canShip: false,
|
||||||
|
canRefund: false,
|
||||||
|
canStoreFilter: true,
|
||||||
|
canStoreInput: true,
|
||||||
|
canDoctorInput: true,
|
||||||
|
},
|
||||||
|
tabList: [
|
||||||
|
{ text: '工作台', iconPath: '/static/tabbar/home.png', selectedIconPath: '/static/tabbar/home-active.png', pagePath: SALESPERSON_HOME },
|
||||||
|
{ text: '我的', iconPath: '/static/tabbar/my.png', selectedIconPath: '/static/tabbar/my-active.png', pagePath: SALESPERSON_MY },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 录入列表/详情:超管与业务员共用页面时按 loginMode 选 API */
|
||||||
|
export function getInputBusinessContext() {
|
||||||
|
const mode = uni.getStorageSync('loginMode');
|
||||||
|
if (mode === 'platform_admin') return getPlatformAdminContext();
|
||||||
|
return getSalespersonContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveBusinessContext(injected) {
|
||||||
|
if (injected && injected.api) return injected;
|
||||||
|
return getInputBusinessContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function businessPageRoot(ctx) {
|
||||||
|
return ctx.businessPackageRoot || ctx.packageRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pageUrl(ctx, subPath) {
|
||||||
|
return `${businessPageRoot(ctx)}/${subPath}`;
|
||||||
|
}
|
||||||
202
subPackages/sub_salesperson/common/inputDetailFields.js
Normal file
202
subPackages/sub_salesperson/common/inputDetailFields.js
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
export function inputStatusText(status) {
|
||||||
|
const map = { 0: '待审核', 1: '审核通过', 2: '审核拒绝' }
|
||||||
|
return map[Number(status)] ?? '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusText(status) {
|
||||||
|
return inputStatusText(status)
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(ts) {
|
||||||
|
if (!ts) return '-'
|
||||||
|
const d = new Date(Number(ts) * 1000)
|
||||||
|
const pad = (n) => String(n).padStart(2, '0')
|
||||||
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function storeTypeText(type) {
|
||||||
|
return Number(type) === 0 ? '诊所' : '药店'
|
||||||
|
}
|
||||||
|
|
||||||
|
function clinicTypeText(type) {
|
||||||
|
if (Number(type) === 1) return '西医诊所'
|
||||||
|
if (Number(type) === 2) return '中医诊所'
|
||||||
|
return '未设置'
|
||||||
|
}
|
||||||
|
|
||||||
|
function doctorTypeText(type) {
|
||||||
|
if (Number(type) === 1) return '中医医生'
|
||||||
|
if (Number(type) === 2) return '西医医生'
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
function signTypeText(signType) {
|
||||||
|
if (Number(signType) === 1) return '电子签名'
|
||||||
|
if (Number(signType) === 2) return '手写签名'
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
function yesNoText(v) {
|
||||||
|
return Number(v) === 1 ? '是' : '否'
|
||||||
|
}
|
||||||
|
|
||||||
|
function regionText(info) {
|
||||||
|
const p = info.province?.name || ''
|
||||||
|
const c = info.city?.name || ''
|
||||||
|
return [p, c].filter(Boolean).join(' ') || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
function storeNamesText(info) {
|
||||||
|
const stores = info.stores
|
||||||
|
if (Array.isArray(stores) && stores.length) {
|
||||||
|
const names = stores.map((s) => s.store?.name || s.name).filter(Boolean)
|
||||||
|
if (names.length) return names.join('、')
|
||||||
|
}
|
||||||
|
if (info.store?.name) return info.store.name
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
function promoterText(info) {
|
||||||
|
const u = info.promoter || info.inputUser
|
||||||
|
if (!u) return info.code || '-'
|
||||||
|
return [u.nick_name, u.code].filter(Boolean).join(' · ') || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
function bankTypeText(type) {
|
||||||
|
return Number(type) === 1 ? '对公' : '对私'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getStoreDetailSections() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: '基础信息',
|
||||||
|
fields: [
|
||||||
|
{ key: 'id', label: 'ID' },
|
||||||
|
{ key: 'name', label: '门店名称' },
|
||||||
|
{ key: 'type', label: '门店类型', format: (_, info) => storeTypeText(info.type) },
|
||||||
|
{ key: 'clinic_type', label: '诊所类型', format: (_, info) => clinicTypeText(info.clinic_type) },
|
||||||
|
{ key: 'contact', label: '联系人' },
|
||||||
|
{ key: 'mobile', label: '联系电话' },
|
||||||
|
{ key: 'region', label: '省市区', format: (_, info) => regionText(info) },
|
||||||
|
{ key: 'position', label: '详细地址' },
|
||||||
|
{ key: 'code', label: '业务员', format: (_, info) => promoterText(info) },
|
||||||
|
{ key: 'is_shipping_free', label: '是否包邮', format: (_, info) => yesNoText(info.is_shipping_free) },
|
||||||
|
{ key: 'subscribe_price_change', label: '订阅价格波动', format: (_, info) => yesNoText(info.subscribe_price_change) },
|
||||||
|
{
|
||||||
|
key: 'business_hours',
|
||||||
|
label: '营业时间',
|
||||||
|
format: (_, info) => {
|
||||||
|
const s = info.start_time || ''
|
||||||
|
const e = info.end_time || ''
|
||||||
|
return s && e ? `${s} - ${e}` : '-'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '结算信息',
|
||||||
|
fields: [
|
||||||
|
{ key: 'bank_type', label: '账户类型', format: (_, info) => bankTypeText(info.bank_type) },
|
||||||
|
{ key: 'bank_user_name', label: '开户人' },
|
||||||
|
{ key: 'bank_card', label: '银行卡号' },
|
||||||
|
{ key: 'bank_name', label: '开户行' },
|
||||||
|
{ key: 'bank_no', label: '银联行号' },
|
||||||
|
{ key: 'z_buy_percent', label: '中药采购比例(%)' },
|
||||||
|
{ key: 'z_sale_percent', label: '中药销售比例(%)' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '审核信息',
|
||||||
|
fields: [
|
||||||
|
{ key: 'inputUser', label: '录入人', format: (_, info) => info.inputUser?.nick_name || '-' },
|
||||||
|
{ key: 'status', label: '审核状态', format: (_, info) => statusText(info.status) },
|
||||||
|
{ key: 'auditAdmin', label: '审核人', format: (_, info) => info.auditAdmin?.nick_name || '-', show: (info) => !!info.auditAdmin },
|
||||||
|
{ key: 'audit_time', label: '审核时间', format: (_, info) => formatTime(info.audit_time), show: (info) => !!info.audit_time },
|
||||||
|
{ key: 'audit_remark', label: '审核备注', show: (info) => !!info.audit_remark },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '图片资料',
|
||||||
|
fields: [
|
||||||
|
{ key: 'see_rate', label: '公章', type: 'image' },
|
||||||
|
{ key: 'url', label: '轮播图', type: 'images' },
|
||||||
|
{ key: 'contract_files', label: '合同文件', type: 'files' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDoctorDetailSections() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: '基础信息',
|
||||||
|
fields: [
|
||||||
|
{ key: 'id', label: 'ID' },
|
||||||
|
{ key: 'name', label: '医生姓名' },
|
||||||
|
{ key: 'mobile', label: '手机号' },
|
||||||
|
{ key: 'idcard', label: '身份证' },
|
||||||
|
{ key: 'type', label: '身份', format: (_, info) => doctorTypeText(info.type) },
|
||||||
|
{ key: 'sign_type', label: '签名类型', format: (_, info) => signTypeText(info.sign_type) },
|
||||||
|
{ key: 'depart', label: '科室', format: (_, info) => info.depart?.name || '-' },
|
||||||
|
{ key: 'titleInfo', label: '职称', format: (_, info) => info.titleInfo?.name || '-' },
|
||||||
|
{ key: 'stores', label: '关联门店', format: (_, info) => storeNamesText(info) },
|
||||||
|
{ key: 'good_at', label: '擅长', multiline: true },
|
||||||
|
{ key: 'intro', label: '简介', multiline: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '审核信息',
|
||||||
|
fields: [
|
||||||
|
{ key: 'inputUser', label: '录入人', format: (_, info) => info.inputUser?.nick_name || '-' },
|
||||||
|
{ key: 'status', label: '审核状态', format: (_, info) => statusText(info.status) },
|
||||||
|
{ key: 'auditAdmin', label: '审核人', format: (_, info) => info.auditAdmin?.nick_name || '-', show: (info) => !!info.auditAdmin },
|
||||||
|
{ key: 'audit_time', label: '审核时间', format: (_, info) => formatTime(info.audit_time), show: (info) => !!info.audit_time },
|
||||||
|
{ key: 'audit_remark', label: '审核备注', show: (info) => !!info.audit_remark },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '证照资料',
|
||||||
|
fields: [
|
||||||
|
{ key: 'avatar', label: '头像', type: 'image' },
|
||||||
|
{ key: 'qualification', label: '资格证书', type: 'image' },
|
||||||
|
{ key: 'practicing', label: '执业证书', type: 'image' },
|
||||||
|
{ key: 'title', label: '职称证书', type: 'image' },
|
||||||
|
{ key: 'card_up', label: '身份证正面', type: 'image' },
|
||||||
|
{ key: 'card_down', label: '身份证反面', type: 'image' },
|
||||||
|
{ key: 'sign_image', label: '签名图片', type: 'image' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDetailFieldValue(field, info) {
|
||||||
|
if (field.show && !field.show(info)) return null
|
||||||
|
if (field.format) return field.format(field.key, info)
|
||||||
|
const v = info[field.key]
|
||||||
|
if (v == null || v === '') return '-'
|
||||||
|
if (Array.isArray(v) && !v.length) return null
|
||||||
|
return String(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDetailImageUrls(field, info) {
|
||||||
|
const v = info[field.key]
|
||||||
|
if (!v) return []
|
||||||
|
if (field.type === 'images' && Array.isArray(v)) return v.filter(Boolean)
|
||||||
|
if (field.type === 'image' && typeof v === 'string') return [v]
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 门店详情关联的医生预填列表 */
|
||||||
|
export function getLinkedDoctorInputs(info) {
|
||||||
|
const list = info?.doctor_inputs ?? info?.doctorInputs ?? []
|
||||||
|
return Array.isArray(list) ? list : []
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDetailFileLinks(field, info) {
|
||||||
|
const files = info[field.key]
|
||||||
|
if (!Array.isArray(files) || !files.length) return []
|
||||||
|
return files.map((f) => {
|
||||||
|
if (typeof f === 'string') return { url: f, name: f.split('/').pop() || '合同文件' }
|
||||||
|
return { url: f.file_url || f.url || '', name: f.file_name || f.name || '合同文件' }
|
||||||
|
}).filter((f) => f.url)
|
||||||
|
}
|
||||||
56
subPackages/sub_salesperson/common/inputDraftHelpers.js
Normal file
56
subPackages/sub_salesperson/common/inputDraftHelpers.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import {
|
||||||
|
createDefaultStoreInputForm,
|
||||||
|
createDefaultDoctorInputForm,
|
||||||
|
} from '@/subPackages/sub_salesperson/common/inputFormHelpers.js'
|
||||||
|
|
||||||
|
export function serializeStoreDraft(vm) {
|
||||||
|
return {
|
||||||
|
form: JSON.parse(JSON.stringify(vm.form)),
|
||||||
|
ui: {
|
||||||
|
currentStep: vm.currentStep,
|
||||||
|
clinicTypeIndex: vm.clinicTypeIndex,
|
||||||
|
bankTypeIndex: vm.bankTypeIndex,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyStoreDraft(vm, draft) {
|
||||||
|
if (!draft) return
|
||||||
|
Object.assign(vm.form, createDefaultStoreInputForm(), draft.form || {})
|
||||||
|
if (!Array.isArray(vm.form.url)) vm.form.url = []
|
||||||
|
if (!Array.isArray(vm.form.contract_files)) vm.form.contract_files = []
|
||||||
|
const ui = draft.ui || {}
|
||||||
|
vm.currentStep = ui.currentStep != null ? ui.currentStep : 0
|
||||||
|
vm.clinicTypeIndex = ui.clinicTypeIndex != null ? ui.clinicTypeIndex : (Number(vm.form.clinic_type) === 1 ? 0 : 1)
|
||||||
|
vm.bankTypeIndex = ui.bankTypeIndex != null
|
||||||
|
? ui.bankTypeIndex
|
||||||
|
: Math.max(0, vm.bankTypes.findIndex((b) => b.value === Number(vm.form.bank_account_type)))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyDoctorDraft(vm, draft) {
|
||||||
|
if (!draft) return
|
||||||
|
Object.assign(vm.form, createDefaultDoctorInputForm(), draft.form || {})
|
||||||
|
const ui = draft.ui || {}
|
||||||
|
vm.selectedStoreInputIds = Array.isArray(ui.selectedStoreInputIds)
|
||||||
|
? [...ui.selectedStoreInputIds]
|
||||||
|
: (Array.isArray(ui.selectedStoreIds) ? [...ui.selectedStoreIds] : [])
|
||||||
|
vm.departIndex = ui.departIndex != null ? ui.departIndex : Math.max(0, vm.departOptions.findIndex((d) => d.id === vm.form.depart_id))
|
||||||
|
vm.titleIndex = ui.titleIndex != null ? ui.titleIndex : Math.max(0, vm.titleOptions.findIndex((t) => t.id === vm.form.title_id))
|
||||||
|
vm.typeIndex = ui.typeIndex != null ? ui.typeIndex : (Number(vm.form.type) === 2 ? 1 : 0)
|
||||||
|
vm.signTypeIndex = ui.signTypeIndex != null ? ui.signTypeIndex : (Number(vm.form.sign_type) === 2 ? 1 : 0)
|
||||||
|
vm.currentStep = ui.currentStep != null ? ui.currentStep : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
export function serializeDoctorDraft(vm) {
|
||||||
|
return {
|
||||||
|
form: JSON.parse(JSON.stringify(vm.form)),
|
||||||
|
ui: {
|
||||||
|
selectedStoreInputIds: [...(vm.selectedStoreInputIds || vm.selectedStoreIds || [])],
|
||||||
|
departIndex: vm.departIndex,
|
||||||
|
titleIndex: vm.titleIndex,
|
||||||
|
typeIndex: vm.typeIndex,
|
||||||
|
signTypeIndex: vm.signTypeIndex,
|
||||||
|
currentStep: vm.currentStep,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
109
subPackages/sub_salesperson/common/inputDraftStorage.js
Normal file
109
subPackages/sub_salesperson/common/inputDraftStorage.js
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
/**
|
||||||
|
* 业务员录入本地草稿(uni.setStorageSync)
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const MAX_DRAFTS = 20
|
||||||
|
|
||||||
|
const STORAGE_VERSION = 'v1'
|
||||||
|
|
||||||
|
function readJson(key) {
|
||||||
|
try {
|
||||||
|
const raw = uni.getStorageSync(key)
|
||||||
|
if (!raw) return []
|
||||||
|
return typeof raw === 'string' ? JSON.parse(raw) : raw
|
||||||
|
} catch (e) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeJson(key, data) {
|
||||||
|
try {
|
||||||
|
uni.setStorageSync(key, JSON.stringify(data))
|
||||||
|
return true
|
||||||
|
} catch (e) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function storageUserSuffix() {
|
||||||
|
const user = uni.getStorageSync('salesperson_user') || {}
|
||||||
|
if (user.id != null && user.id !== '') return String(user.id)
|
||||||
|
const token = uni.getStorageSync('salesperson_token') || ''
|
||||||
|
if (!token) return 'anon'
|
||||||
|
let hash = 0
|
||||||
|
for (let i = 0; i < token.length; i += 1) {
|
||||||
|
hash = (hash * 31 + token.charCodeAt(i)) | 0
|
||||||
|
}
|
||||||
|
return 't' + Math.abs(hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
function storageKey(type) {
|
||||||
|
return `salesperson.inputDraft.${STORAGE_VERSION}.${storageUserSuffix()}.${type}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function newDraftId() {
|
||||||
|
return `draft_${Date.now()}_${Math.floor(Math.random() * 10000)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listDrafts(type) {
|
||||||
|
const list = readJson(storageKey(type))
|
||||||
|
if (!Array.isArray(list)) return []
|
||||||
|
return list.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDraft(type, draftId) {
|
||||||
|
if (!draftId) return null
|
||||||
|
return listDrafts(type).find((d) => d.id === draftId) || null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeDraft(type, draftId) {
|
||||||
|
const key = storageKey(type)
|
||||||
|
const list = listDrafts(type).filter((d) => d.id !== draftId)
|
||||||
|
writeJson(key, list)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveDraft(type, { name, form, ui, draftId }) {
|
||||||
|
const trimmed = String(name || '').trim()
|
||||||
|
if (!trimmed) {
|
||||||
|
throw new Error('请输入草稿名称')
|
||||||
|
}
|
||||||
|
const key = storageKey(type)
|
||||||
|
let list = listDrafts(type)
|
||||||
|
const now = Math.floor(Date.now() / 1000)
|
||||||
|
const payload = {
|
||||||
|
id: draftId || newDraftId(),
|
||||||
|
name: trimmed,
|
||||||
|
updatedAt: now,
|
||||||
|
form: JSON.parse(JSON.stringify(form || {})),
|
||||||
|
ui: JSON.parse(JSON.stringify(ui || {})),
|
||||||
|
}
|
||||||
|
const idx = list.findIndex((d) => d.id === payload.id)
|
||||||
|
if (idx >= 0) {
|
||||||
|
list[idx] = payload
|
||||||
|
} else {
|
||||||
|
if (list.length >= MAX_DRAFTS) {
|
||||||
|
throw new Error(`最多保存 ${MAX_DRAFTS} 个草稿,请先删除旧草稿`)
|
||||||
|
}
|
||||||
|
list.push(payload)
|
||||||
|
}
|
||||||
|
const ok = writeJson(key, list)
|
||||||
|
if (!ok) {
|
||||||
|
throw new Error('保存失败,存储空间可能不足,请删除部分草稿或图片后再试')
|
||||||
|
}
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDraftTime(ts) {
|
||||||
|
if (!ts) return ''
|
||||||
|
const d = new Date(Number(ts) * 1000)
|
||||||
|
const pad = (n) => String(n).padStart(2, '0')
|
||||||
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function draftSummary(type, form) {
|
||||||
|
if (!form) return ''
|
||||||
|
if (type === 'store') {
|
||||||
|
return form.name || form.contact || form.mobile || '未填写名称'
|
||||||
|
}
|
||||||
|
return form.name || form.mobile || '未填写姓名'
|
||||||
|
}
|
||||||
195
subPackages/sub_salesperson/common/inputFormHelpers.js
Normal file
195
subPackages/sub_salesperson/common/inputFormHelpers.js
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
import { prepareImagePath } from '@/common/js/image-compress.js'
|
||||||
|
import { uploadLegacyAdminImage, uploadBusinessChatFileApi, extractUploadUrl } from '@/api/upload.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选择并上传图片(压缩 + old-admin-img 接口)
|
||||||
|
* @param {{ count?: number }} options count>1 时依次上传多张,返回 url 数组
|
||||||
|
*/
|
||||||
|
export function pickAndUploadImage(options = {}) {
|
||||||
|
const count = Math.min(Math.max(options.count || 1, 1), 9)
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
uni.chooseImage({
|
||||||
|
count,
|
||||||
|
sizeType: ['original', 'compressed'],
|
||||||
|
sourceType: ['album', 'camera'],
|
||||||
|
success: async (res) => {
|
||||||
|
uni.showLoading({ title: '上传中...', mask: true })
|
||||||
|
try {
|
||||||
|
const files = res.tempFiles || []
|
||||||
|
const paths = files.length
|
||||||
|
? files.map((f) => f.path)
|
||||||
|
: (res.tempFilePaths || [])
|
||||||
|
const urls = []
|
||||||
|
for (const path of paths) {
|
||||||
|
const prepared = await prepareImagePath({
|
||||||
|
path,
|
||||||
|
size: files.find((f) => f.path === path)?.size,
|
||||||
|
})
|
||||||
|
if (!prepared) continue
|
||||||
|
const url = await uploadLegacyAdminImage({
|
||||||
|
filePath: prepared.path,
|
||||||
|
name: 'file',
|
||||||
|
})
|
||||||
|
urls.push(url)
|
||||||
|
}
|
||||||
|
uni.hideLoading()
|
||||||
|
if (!urls.length) {
|
||||||
|
reject(new Error('上传失败'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resolve(count === 1 ? urls[0] : urls)
|
||||||
|
} catch (e) {
|
||||||
|
uni.hideLoading()
|
||||||
|
uni.showToast({ title: e.message || '上传失败', icon: 'none' })
|
||||||
|
reject(e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
if (err && err.errMsg && err.errMsg.indexOf('cancel') !== -1) {
|
||||||
|
reject(new Error('cancel'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
reject(err)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUserDataPath() {
|
||||||
|
if (typeof wx !== 'undefined' && wx.env && wx.env.USER_DATA_PATH) {
|
||||||
|
return wx.env.USER_DATA_PATH
|
||||||
|
}
|
||||||
|
if (uni.env && uni.env.USER_DATA_PATH) {
|
||||||
|
return uni.env.USER_DATA_PATH
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadSignFilePath(filePath) {
|
||||||
|
let url = null
|
||||||
|
try {
|
||||||
|
const res = await uploadBusinessChatFileApi({ filePath, name: 'file' })
|
||||||
|
url = extractUploadUrl(res)
|
||||||
|
} catch (e) {
|
||||||
|
// 回退与证照相同的上传接口
|
||||||
|
}
|
||||||
|
if (!url) {
|
||||||
|
url = await uploadLegacyAdminImage({ filePath, name: 'file' })
|
||||||
|
}
|
||||||
|
if (!url) {
|
||||||
|
throw new Error('上传成功但未返回地址')
|
||||||
|
}
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手写签名 canvas 临时文件直传 OSS
|
||||||
|
* @param {string} tempFilePath canvasToTempFilePath 路径
|
||||||
|
*/
|
||||||
|
export function uploadSignCanvasTempPath(tempFilePath) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
if (!tempFilePath) {
|
||||||
|
reject(new Error('签名为空'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.showLoading({ title: '上传中...', mask: true })
|
||||||
|
try {
|
||||||
|
const url = await uploadSignFilePath(tempFilePath)
|
||||||
|
uni.hideLoading()
|
||||||
|
resolve(url)
|
||||||
|
} catch (e) {
|
||||||
|
uni.hideLoading()
|
||||||
|
reject(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手写签名板 base64 写入临时文件后上传 OSS
|
||||||
|
* @param {string} base64 canvas 导出的 base64(可带 data:image 前缀)
|
||||||
|
*/
|
||||||
|
export function uploadSignCanvasBase64(base64) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!base64) {
|
||||||
|
reject(new Error('签名为空'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const root = getUserDataPath()
|
||||||
|
if (!root) {
|
||||||
|
reject(new Error('无法获取临时目录'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const fs = uni.getFileSystemManager()
|
||||||
|
const filePath = `${root}/sign_${Date.now()}.png`
|
||||||
|
const data = String(base64).replace(/^data:image\/\w+;base64,/, '')
|
||||||
|
fs.writeFile({
|
||||||
|
filePath,
|
||||||
|
data,
|
||||||
|
encoding: 'base64',
|
||||||
|
success: async () => {
|
||||||
|
uni.showLoading({ title: '上传中...', mask: true })
|
||||||
|
try {
|
||||||
|
const url = await uploadSignFilePath(filePath)
|
||||||
|
uni.hideLoading()
|
||||||
|
resolve(url)
|
||||||
|
} catch (e) {
|
||||||
|
uni.hideLoading()
|
||||||
|
reject(e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: () => reject(new Error('签名临时文件写入失败')),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createDefaultStoreInputForm() {
|
||||||
|
return {
|
||||||
|
name: '',
|
||||||
|
contact: '',
|
||||||
|
mobile: '',
|
||||||
|
code: '',
|
||||||
|
position: '',
|
||||||
|
province_id: null,
|
||||||
|
city_id: null,
|
||||||
|
clinic_type: 2,
|
||||||
|
type: 0,
|
||||||
|
is_shipping_free: 0,
|
||||||
|
subscribe_price_change: 0,
|
||||||
|
start_time: '08:00',
|
||||||
|
end_time: '20:00',
|
||||||
|
see_rate: '',
|
||||||
|
url: [],
|
||||||
|
contract_files: [],
|
||||||
|
z_buy_percent: 100,
|
||||||
|
z_sale_percent: 100,
|
||||||
|
bank_user_name: '',
|
||||||
|
bank_card: '',
|
||||||
|
bank_name: '',
|
||||||
|
bank_no: '',
|
||||||
|
bank_account_type: 1,
|
||||||
|
address: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createDefaultDoctorInputForm() {
|
||||||
|
return {
|
||||||
|
name: '',
|
||||||
|
mobile: '',
|
||||||
|
idcard: '',
|
||||||
|
avatar: '',
|
||||||
|
depart_id: null,
|
||||||
|
title_id: null,
|
||||||
|
type: 1,
|
||||||
|
sign_type: 1,
|
||||||
|
good_at: '',
|
||||||
|
intro: '',
|
||||||
|
qualification: '',
|
||||||
|
practicing: '',
|
||||||
|
title: '',
|
||||||
|
card_up: '',
|
||||||
|
card_down: '',
|
||||||
|
sign_image: '',
|
||||||
|
store_id: 0,
|
||||||
|
store_ids: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
42
subPackages/sub_salesperson/common/inputNavigation.js
Normal file
42
subPackages/sub_salesperson/common/inputNavigation.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { listDrafts } from '@/subPackages/sub_salesperson/common/inputDraftStorage.js'
|
||||||
|
|
||||||
|
export function resolveDoctorFormPath(bizCtx) {
|
||||||
|
const root = (bizCtx && bizCtx.packageRoot) || '/subPackages/sub_salesperson'
|
||||||
|
return `${root}/doctor-input/form`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function goDoctorInputFromStore(storeInputId, bizCtx) {
|
||||||
|
const id = Number(storeInputId || 0)
|
||||||
|
if (!id) return
|
||||||
|
const path = resolveDoctorFormPath(bizCtx)
|
||||||
|
uni.navigateTo({ url: `${path}?storeInputId=${id}` })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function navigateToInputForm(formPath, inputType, callbacks = {}) {
|
||||||
|
const drafts = listDrafts(inputType)
|
||||||
|
if (!drafts.length) {
|
||||||
|
uni.navigateTo({ url: formPath })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.showActionSheet({
|
||||||
|
itemList: ['空白新建', '从草稿继续…'],
|
||||||
|
success: (res) => {
|
||||||
|
if (res.tapIndex === 0) {
|
||||||
|
uni.navigateTo({ url: formPath })
|
||||||
|
} else if (res.tapIndex === 1) {
|
||||||
|
if (typeof callbacks.onPickDraft === 'function') {
|
||||||
|
callbacks.onPickDraft()
|
||||||
|
} else {
|
||||||
|
const listPath = formPath.replace('/form', '/index')
|
||||||
|
uni.navigateTo({ url: `${listPath}?openDraft=1` })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openDraftForm(formPath, draftId) {
|
||||||
|
if (!draftId) return
|
||||||
|
const sep = formPath.indexOf('?') >= 0 ? '&' : '?'
|
||||||
|
uni.navigateTo({ url: `${formPath}${sep}draftId=${encodeURIComponent(draftId)}` })
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
104
subPackages/sub_salesperson/components/DepartPicker.vue
Normal file
104
subPackages/sub_salesperson/components/DepartPicker.vue
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
<template>
|
||||||
|
<view class="picker-wrap">
|
||||||
|
<view class="depart-picker" @click="onOpen">
|
||||||
|
<text :class="selected ? 'value-text' : 'placeholder-text'">{{ displayLabel }}</text>
|
||||||
|
<u-icon name="arrow-right" color="#c0c4cc" size="28"></u-icon>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<drawer-page-container v-model="showPopup" title="选择科室">
|
||||||
|
<view class="search-wrap">
|
||||||
|
<u-search
|
||||||
|
v-model="keyword"
|
||||||
|
placeholder="搜索科室名称"
|
||||||
|
:show-action="false"
|
||||||
|
shape="round"
|
||||||
|
bg-color="#f5f5f5"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<scroll-view scroll-y class="sheet-scroll">
|
||||||
|
<view v-if="loading" class="empty">加载中...</view>
|
||||||
|
<view v-else-if="!filteredOptions.length" class="empty">{{ keyword ? '无匹配科室' : '暂无科室' }}</view>
|
||||||
|
<view
|
||||||
|
v-for="item in filteredOptions"
|
||||||
|
:key="item.id"
|
||||||
|
class="option-item"
|
||||||
|
@click="onPick(item)"
|
||||||
|
>
|
||||||
|
<text class="name">{{ item.name }}</text>
|
||||||
|
<u-icon v-if="Number(value) === Number(item.id)" name="checkmark" color="#6acdbb" size="32"></u-icon>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</drawer-page-container>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import DrawerPageContainer from '@/subPackages/sub_salesperson/components/DrawerPageContainer.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DepartPicker',
|
||||||
|
components: { DrawerPageContainer },
|
||||||
|
props: {
|
||||||
|
value: { type: [Number, String], default: null },
|
||||||
|
options: { type: Array, default: () => [] },
|
||||||
|
loading: { type: Boolean, default: false },
|
||||||
|
placeholder: { type: String, default: '请选择科室' },
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showPopup: false,
|
||||||
|
keyword: '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
selected() {
|
||||||
|
return this.options.find((o) => Number(o.id) === Number(this.value)) || null
|
||||||
|
},
|
||||||
|
displayLabel() {
|
||||||
|
return this.selected ? this.selected.name : this.placeholder
|
||||||
|
},
|
||||||
|
filteredOptions() {
|
||||||
|
const kw = (this.keyword || '').trim().toLowerCase()
|
||||||
|
if (!kw) return this.options
|
||||||
|
return this.options.filter((o) => String(o.name || '').toLowerCase().includes(kw))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onOpen() {
|
||||||
|
this.keyword = ''
|
||||||
|
this.showPopup = true
|
||||||
|
},
|
||||||
|
onPick(item) {
|
||||||
|
this.$emit('input', item.id)
|
||||||
|
this.$emit('change', item)
|
||||||
|
this.showPopup = false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.picker-wrap { width: 100%; }
|
||||||
|
.depart-picker {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 88rpx;
|
||||||
|
padding: 8rpx 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.value-text { font-size: 28rpx; color: #1d2129; flex: 1; }
|
||||||
|
.placeholder-text { font-size: 28rpx; color: #c0c4cc; flex: 1; }
|
||||||
|
.search-wrap { padding: 16rpx 24rpx 8rpx; flex-shrink: 0; }
|
||||||
|
.sheet-scroll { max-height: 60vh; padding-bottom: 24rpx; }
|
||||||
|
.option-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 24rpx 32rpx;
|
||||||
|
border-bottom: 1rpx solid #f5f5f5;
|
||||||
|
}
|
||||||
|
.name { font-size: 28rpx; color: #1d2129; }
|
||||||
|
.empty { text-align: center; color: #86909c; padding: 80rpx 0; font-size: 28rpx; }
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<template>
|
||||||
|
<view>
|
||||||
|
<!-- #ifdef MP-WEIXIN -->
|
||||||
|
<page-container
|
||||||
|
v-if="show"
|
||||||
|
:show="show"
|
||||||
|
:overlay="false"
|
||||||
|
@leave="onPageLeave"
|
||||||
|
/>
|
||||||
|
<!-- #endif -->
|
||||||
|
<u-popup
|
||||||
|
:value="show"
|
||||||
|
mode="bottom"
|
||||||
|
border-radius="24"
|
||||||
|
:safe-area-inset-bottom="true"
|
||||||
|
:mask-close-able="maskCloseable"
|
||||||
|
@input="onPopupInput"
|
||||||
|
@close="close"
|
||||||
|
>
|
||||||
|
<view class="drawer-inner">
|
||||||
|
<view v-if="title" class="drawer-header">
|
||||||
|
<text class="drawer-title">{{ title }}</text>
|
||||||
|
<text v-if="showClose" class="drawer-close" @click="close">关闭</text>
|
||||||
|
</view>
|
||||||
|
<slot />
|
||||||
|
</view>
|
||||||
|
</u-popup>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'DrawerPageContainer',
|
||||||
|
props: {
|
||||||
|
value: { type: Boolean, default: false },
|
||||||
|
title: { type: String, default: '' },
|
||||||
|
showClose: { type: Boolean, default: true },
|
||||||
|
maskCloseable: { type: Boolean, default: true },
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
show: {
|
||||||
|
get() { return this.value },
|
||||||
|
set(v) { this.$emit('input', v) },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
close() {
|
||||||
|
this.show = false
|
||||||
|
this.$emit('close')
|
||||||
|
},
|
||||||
|
onPopupInput(v) {
|
||||||
|
this.show = v
|
||||||
|
},
|
||||||
|
onPageLeave() {
|
||||||
|
this.close()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.drawer-inner { background: #fff; max-height: 80vh; display: flex; flex-direction: column; }
|
||||||
|
.drawer-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 28rpx 32rpx;
|
||||||
|
border-bottom: 1rpx solid #f0f0f0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.drawer-title { font-size: 32rpx; font-weight: 600; color: #1d2129; }
|
||||||
|
.drawer-close { font-size: 28rpx; color: #6acdbb; }
|
||||||
|
</style>
|
||||||
104
subPackages/sub_salesperson/components/InputDraftSheet.vue
Normal file
104
subPackages/sub_salesperson/components/InputDraftSheet.vue
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
<template>
|
||||||
|
<u-popup v-model="visible" mode="bottom" border-radius="24" :safe-area-inset-bottom="true">
|
||||||
|
<view class="sheet">
|
||||||
|
<view class="sheet-header">
|
||||||
|
<text class="sheet-title">选择草稿</text>
|
||||||
|
<text class="sheet-close" @click="close">关闭</text>
|
||||||
|
</view>
|
||||||
|
<scroll-view scroll-y class="sheet-scroll">
|
||||||
|
<view v-if="!drafts.length" class="empty">暂无本地草稿</view>
|
||||||
|
<view
|
||||||
|
v-for="item in drafts"
|
||||||
|
:key="item.id"
|
||||||
|
class="draft-item"
|
||||||
|
@click="onSelect(item)"
|
||||||
|
>
|
||||||
|
<view class="draft-main">
|
||||||
|
<text class="draft-name">{{ item.name }}</text>
|
||||||
|
<text class="draft-sub">{{ summary(item) }}</text>
|
||||||
|
<text class="draft-time">{{ formatTime(item.updatedAt) }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="draft-del" @click.stop="onDelete(item)">删除</text>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</u-popup>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
listDrafts,
|
||||||
|
removeDraft,
|
||||||
|
formatDraftTime,
|
||||||
|
draftSummary,
|
||||||
|
} from '@/subPackages/sub_salesperson/common/inputDraftStorage.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'InputDraftSheet',
|
||||||
|
props: {
|
||||||
|
value: { type: Boolean, default: false },
|
||||||
|
inputType: { type: String, default: 'store' },
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
drafts: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
visible: {
|
||||||
|
get() { return this.value },
|
||||||
|
set(v) { this.$emit('input', v) },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
value(v) {
|
||||||
|
if (v) this.refresh()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
refresh() {
|
||||||
|
this.drafts = listDrafts(this.inputType)
|
||||||
|
},
|
||||||
|
formatTime(ts) {
|
||||||
|
return formatDraftTime(ts)
|
||||||
|
},
|
||||||
|
summary(item) {
|
||||||
|
return draftSummary(this.inputType, item.form)
|
||||||
|
},
|
||||||
|
close() {
|
||||||
|
this.visible = false
|
||||||
|
},
|
||||||
|
onSelect(item) {
|
||||||
|
this.$emit('select', item)
|
||||||
|
this.close()
|
||||||
|
},
|
||||||
|
onDelete(item) {
|
||||||
|
uni.showModal({
|
||||||
|
title: '删除草稿',
|
||||||
|
content: `确定删除「${item.name}」?`,
|
||||||
|
success: (res) => {
|
||||||
|
if (!res.confirm) return
|
||||||
|
removeDraft(this.inputType, item.id)
|
||||||
|
this.refresh()
|
||||||
|
this.$emit('deleted', item.id)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.sheet { background: #fff; max-height: 70vh; display: flex; flex-direction: column; }
|
||||||
|
.sheet-header { display: flex; justify-content: space-between; align-items: center; padding: 28rpx 32rpx; border-bottom: 1rpx solid #f0f0f0; }
|
||||||
|
.sheet-title { font-size: 32rpx; font-weight: 600; color: #1d2129; }
|
||||||
|
.sheet-close { font-size: 28rpx; color: #6acdbb; }
|
||||||
|
.sheet-scroll { max-height: 60vh; padding: 16rpx 24rpx 40rpx; box-sizing: border-box; }
|
||||||
|
.empty { text-align: center; color: #86909c; padding: 80rpx 0; font-size: 28rpx; }
|
||||||
|
.draft-item { display: flex; align-items: center; padding: 24rpx; margin-bottom: 16rpx; background: #f7f8fa; border-radius: 12rpx; }
|
||||||
|
.draft-main { flex: 1; min-width: 0; }
|
||||||
|
.draft-name { display: block; font-size: 30rpx; font-weight: 600; color: #1d2129; }
|
||||||
|
.draft-sub { display: block; font-size: 26rpx; color: #4e5969; margin-top: 8rpx; }
|
||||||
|
.draft-time { display: block; font-size: 24rpx; color: #86909c; margin-top: 8rpx; }
|
||||||
|
.draft-del { font-size: 26rpx; color: #f53f3f; padding: 8rpx 16rpx; flex-shrink: 0; }
|
||||||
|
</style>
|
||||||
70
subPackages/sub_salesperson/components/InputFormLayout.vue
Normal file
70
subPackages/sub_salesperson/components/InputFormLayout.vue
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
<template>
|
||||||
|
<view class="input-form-layout">
|
||||||
|
<view class="step-header">
|
||||||
|
<u-steps :list="stepList" :current="currentStep" active-color="#6acdbb"></u-steps>
|
||||||
|
</view>
|
||||||
|
<scroll-view scroll-y class="form-scroll" :style="scrollStyle">
|
||||||
|
<slot />
|
||||||
|
</scroll-view>
|
||||||
|
<view class="footer-actions">
|
||||||
|
<slot name="footer-extra" />
|
||||||
|
<u-button v-if="currentStep > 0" class="btn-prev" @click="$emit('prev')">上一步</u-button>
|
||||||
|
<u-button
|
||||||
|
v-if="currentStep < maxStep"
|
||||||
|
class="btn-next"
|
||||||
|
type="primary"
|
||||||
|
:custom-style="primaryStyle"
|
||||||
|
@click="$emit('next')"
|
||||||
|
>下一步</u-button>
|
||||||
|
<u-button
|
||||||
|
v-if="currentStep === maxStep"
|
||||||
|
class="btn-submit"
|
||||||
|
type="primary"
|
||||||
|
:custom-style="primaryStyle"
|
||||||
|
@click="$emit('submit')"
|
||||||
|
>{{ submitLabel }}</u-button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'InputFormLayout',
|
||||||
|
props: {
|
||||||
|
stepList: { type: Array, required: true },
|
||||||
|
currentStep: { type: Number, default: 0 },
|
||||||
|
submitLabel: { type: String, default: '确认保存' },
|
||||||
|
scrollHeight: { type: String, default: 'calc(100vh - 480rpx)' },
|
||||||
|
primaryStyle: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({
|
||||||
|
backgroundColor: '#6acdbb',
|
||||||
|
color: '#ffffff',
|
||||||
|
borderColor: '#6acdbb',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
maxStep() {
|
||||||
|
return Math.max(0, (this.stepList || []).length - 1)
|
||||||
|
},
|
||||||
|
scrollStyle() {
|
||||||
|
return { height: this.scrollHeight }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.step-header { background-color: #fff; padding: 30rpx 40rpx; margin-bottom: 20rpx; }
|
||||||
|
.form-scroll { box-sizing: border-box; }
|
||||||
|
.footer-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 40rpx 24rpx 80rpx;
|
||||||
|
gap: 24rpx;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.btn-prev { flex: 1; }
|
||||||
|
.btn-next, .btn-submit { flex: 2; }
|
||||||
|
</style>
|
||||||
24
subPackages/sub_salesperson/components/PageStickyHeader.vue
Normal file
24
subPackages/sub_salesperson/components/PageStickyHeader.vue
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page-sticky-header">
|
||||||
|
<slot />
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import businessMixin from '@/subPackages/sub_salesperson/common/businessMixin.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [businessMixin],
|
||||||
|
name: 'PageStickyHeader',
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page-sticky-header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
background: #f9fafb;
|
||||||
|
padding-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
113
subPackages/sub_salesperson/components/PromoterPicker.vue
Normal file
113
subPackages/sub_salesperson/components/PromoterPicker.vue
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
<template>
|
||||||
|
<view class="picker-wrap">
|
||||||
|
<view class="promoter-picker" @click="onOpen">
|
||||||
|
<view v-if="selected" class="promoter-selected">
|
||||||
|
<image v-if="selected.avatar" :src="selected.avatar" class="avatar" mode="aspectFill" />
|
||||||
|
<view v-else class="avatar placeholder">{{ (selected.nick_name || '?').charAt(0) }}</view>
|
||||||
|
<view class="meta">
|
||||||
|
<text class="name">{{ selected.nick_name || '-' }}</text>
|
||||||
|
<text class="sub">{{ selected.code }} · {{ selected.role_name }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<text v-else class="placeholder-text">{{ placeholder }}</text>
|
||||||
|
<u-icon v-if="!disabled" name="arrow-right" color="#c0c4cc" size="28"></u-icon>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<drawer-page-container v-model="showPopup" title="选择业务员">
|
||||||
|
<scroll-view scroll-y class="sheet-scroll">
|
||||||
|
<view v-if="loading" class="empty">加载中...</view>
|
||||||
|
<view v-else-if="!options.length" class="empty">暂无可选业务员</view>
|
||||||
|
<view
|
||||||
|
v-for="item in options"
|
||||||
|
:key="item.id"
|
||||||
|
class="option-item"
|
||||||
|
@click="onPick(item)"
|
||||||
|
>
|
||||||
|
<image v-if="item.avatar" :src="item.avatar" class="avatar" mode="aspectFill" />
|
||||||
|
<view v-else class="avatar placeholder">{{ (item.nick_name || '?').charAt(0) }}</view>
|
||||||
|
<view class="meta">
|
||||||
|
<text class="name">{{ item.nick_name || '-' }}</text>
|
||||||
|
<text class="sub">{{ item.code }} · {{ item.role_name }}</text>
|
||||||
|
</view>
|
||||||
|
<u-icon v-if="value === item.code" name="checkmark" color="#6acdbb" size="32"></u-icon>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</drawer-page-container>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import DrawerPageContainer from '@/subPackages/sub_salesperson/components/DrawerPageContainer.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PromoterPicker',
|
||||||
|
components: { DrawerPageContainer },
|
||||||
|
props: {
|
||||||
|
value: { type: String, default: '' },
|
||||||
|
disabled: { type: Boolean, default: false },
|
||||||
|
placeholder: { type: String, default: '请选择业务员' },
|
||||||
|
options: { type: Array, default: () => [] },
|
||||||
|
loading: { type: Boolean, default: false },
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return { showPopup: false }
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
selected() {
|
||||||
|
return this.options.find((o) => o.code === this.value) || null
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onOpen() {
|
||||||
|
if (this.disabled) return
|
||||||
|
this.showPopup = true
|
||||||
|
},
|
||||||
|
onPick(item) {
|
||||||
|
this.$emit('input', item.code)
|
||||||
|
this.$emit('change', item)
|
||||||
|
this.showPopup = false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.picker-wrap {
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.promoter-picker {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 88rpx;
|
||||||
|
padding: 8rpx 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.promoter-selected { display: flex; align-items: center; gap: 16rpx; flex: 1; min-width: 0; }
|
||||||
|
.avatar { width: 72rpx; height: 72rpx; border-radius: 50%; flex-shrink: 0; }
|
||||||
|
.avatar.placeholder {
|
||||||
|
background: #e8f7f4;
|
||||||
|
color: #6acdbb;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.meta { flex: 1; min-width: 0; }
|
||||||
|
.name { display: block; font-size: 28rpx; color: #1d2129; }
|
||||||
|
.sub { display: block; font-size: 24rpx; color: #86909c; margin-top: 6rpx; }
|
||||||
|
.placeholder-text { font-size: 28rpx; color: #c0c4cc; flex: 1; }
|
||||||
|
.sheet-scroll { max-height: 60vh; padding-bottom: 24rpx; }
|
||||||
|
.option-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16rpx;
|
||||||
|
padding: 24rpx 32rpx;
|
||||||
|
border-bottom: 1rpx solid #f5f5f5;
|
||||||
|
}
|
||||||
|
.empty { text-align: center; color: #86909c; padding: 80rpx 0; font-size: 28rpx; }
|
||||||
|
</style>
|
||||||
196
subPackages/sub_salesperson/components/RegionAddressPicker.vue
Normal file
196
subPackages/sub_salesperson/components/RegionAddressPicker.vue
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
<template>
|
||||||
|
<view class="picker-wrap">
|
||||||
|
<view class="region-trigger" @click="openDrawer">
|
||||||
|
<text :class="displayText ? 'region-text' : 'region-placeholder'">{{ displayText || placeholder }}</text>
|
||||||
|
<u-icon name="arrow-right" color="#c0c4cc" size="28"></u-icon>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<drawer-page-container v-model="show" title="选择省市区">
|
||||||
|
<view class="drawer-body">
|
||||||
|
<view class="search-box">
|
||||||
|
<u-input v-model="keyword" placeholder="搜索省/市/区" clearable />
|
||||||
|
</view>
|
||||||
|
<scroll-view scroll-x class="crumb-scroll">
|
||||||
|
<view class="crumbs">
|
||||||
|
<text
|
||||||
|
v-for="(item, idx) in breadcrumbs"
|
||||||
|
:key="item.rowKey"
|
||||||
|
class="crumb"
|
||||||
|
@click="onCrumb(item)"
|
||||||
|
>{{ item.label }}<text v-if="idx < breadcrumbs.length - 1" class="crumb-sep"> / </text></text>
|
||||||
|
<text v-if="!breadcrumbs.length" class="crumb muted">请选择</text>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
<scroll-view scroll-y class="list-scroll">
|
||||||
|
<view
|
||||||
|
v-for="row in listRows"
|
||||||
|
:key="row.rowKey"
|
||||||
|
class="list-row"
|
||||||
|
@click="onPickRow(row)"
|
||||||
|
>
|
||||||
|
<text class="row-label">{{ isSearching ? row.label : row.node.label }}</text>
|
||||||
|
<text v-if="!isSearching && rowHasNextLevel(row, ctx)" class="row-arrow">›</text>
|
||||||
|
</view>
|
||||||
|
<view v-if="!listRows.length" class="empty">无匹配结果</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</drawer-page-container>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import DrawerPageContainer from '@/subPackages/sub_salesperson/components/DrawerPageContainer.vue'
|
||||||
|
import {
|
||||||
|
formatAddressDisplay,
|
||||||
|
resolveAddressValue,
|
||||||
|
buildAddressOutput,
|
||||||
|
buildBreadcrumbFromContext,
|
||||||
|
getInitialPickerLevel,
|
||||||
|
levelOptionsToRows,
|
||||||
|
searchAddressHits,
|
||||||
|
searchHitsToRows,
|
||||||
|
applySearchHit,
|
||||||
|
rowHasNextLevel,
|
||||||
|
truncateContextToBreadcrumb,
|
||||||
|
getNextLevel,
|
||||||
|
canCompleteAfterSelect,
|
||||||
|
} from '@/subPackages/sub_clinic_salesperson/utils/address-index.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'RegionAddressPicker',
|
||||||
|
components: { DrawerPageContainer },
|
||||||
|
props: {
|
||||||
|
value: { type: Array, default: () => [] },
|
||||||
|
placeholder: { type: String, default: '请选择省市区' },
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
show: false,
|
||||||
|
keyword: '',
|
||||||
|
currentLevel: 'province',
|
||||||
|
ctx: { provinceId: undefined, cityId: undefined, districtId: undefined },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
displayText() {
|
||||||
|
return formatAddressDisplay(this.value)
|
||||||
|
},
|
||||||
|
isSearching() {
|
||||||
|
return !!(this.keyword || '').trim()
|
||||||
|
},
|
||||||
|
breadcrumbs() {
|
||||||
|
return buildBreadcrumbFromContext(this.ctx).map((item) => ({
|
||||||
|
...item,
|
||||||
|
rowKey: item.level + '-' + item.id,
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
listRows() {
|
||||||
|
const rows = this.isSearching
|
||||||
|
? searchHitsToRows(searchAddressHits(this.keyword, this.ctx))
|
||||||
|
: levelOptionsToRows(this.currentLevel, this.ctx)
|
||||||
|
return rows.map((row) => ({
|
||||||
|
...row,
|
||||||
|
rowKey: row.level + '-' + row.node.value,
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
rowHasNextLevel,
|
||||||
|
syncFromValue() {
|
||||||
|
const resolved = resolveAddressValue(this.value)
|
||||||
|
this.ctx = {
|
||||||
|
provinceId: resolved.provinceId,
|
||||||
|
cityId: resolved.cityId,
|
||||||
|
districtId: resolved.districtId,
|
||||||
|
}
|
||||||
|
this.currentLevel = getInitialPickerLevel(resolved)
|
||||||
|
this.keyword = ''
|
||||||
|
},
|
||||||
|
openDrawer() {
|
||||||
|
this.syncFromValue()
|
||||||
|
this.show = true
|
||||||
|
},
|
||||||
|
emitValue() {
|
||||||
|
const out = buildAddressOutput(this.ctx.provinceId, this.ctx.cityId, this.ctx.districtId)
|
||||||
|
if (out) this.$emit('change', out)
|
||||||
|
},
|
||||||
|
onCrumb(item) {
|
||||||
|
this.ctx = truncateContextToBreadcrumb(this.ctx, item)
|
||||||
|
this.currentLevel = item.level
|
||||||
|
this.keyword = ''
|
||||||
|
},
|
||||||
|
onPickRow(row) {
|
||||||
|
if (this.isSearching) {
|
||||||
|
const hit = searchAddressHits(this.keyword, this.ctx).find(
|
||||||
|
(h) => h.level === row.level && h.node.value === row.node.value
|
||||||
|
)
|
||||||
|
if (!hit) return
|
||||||
|
this.ctx = applySearchHit(hit, this.ctx)
|
||||||
|
if (hit.complete) {
|
||||||
|
this.emitValue()
|
||||||
|
this.show = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const next = getNextLevel(hit.level, this.ctx)
|
||||||
|
this.currentLevel = next || hit.level
|
||||||
|
this.keyword = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const pickCtx = row.level === 'city'
|
||||||
|
? { provinceId: this.ctx.provinceId, cityId: row.node.value }
|
||||||
|
: row.level === 'district'
|
||||||
|
? { provinceId: this.ctx.provinceId, cityId: this.ctx.cityId, districtId: row.node.value }
|
||||||
|
: { provinceId: row.node.value, cityId: undefined, districtId: undefined }
|
||||||
|
|
||||||
|
if (row.level === 'province') {
|
||||||
|
this.ctx = { provinceId: row.node.value, cityId: undefined, districtId: undefined }
|
||||||
|
this.currentLevel = getNextLevel('province', this.ctx) || 'province'
|
||||||
|
} else if (row.level === 'city') {
|
||||||
|
this.ctx = { ...this.ctx, cityId: row.node.value, districtId: undefined }
|
||||||
|
if (canCompleteAfterSelect('city', pickCtx, row.node)) {
|
||||||
|
this.emitValue()
|
||||||
|
this.show = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.currentLevel = getNextLevel('city', this.ctx) || 'city'
|
||||||
|
} else if (row.level === 'district') {
|
||||||
|
this.ctx = { ...this.ctx, districtId: row.node.value }
|
||||||
|
this.emitValue()
|
||||||
|
this.show = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.picker-wrap { width: 100%; }
|
||||||
|
.region-trigger {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 72rpx;
|
||||||
|
}
|
||||||
|
.region-text { font-size: 28rpx; color: #303133; flex: 1; }
|
||||||
|
.region-placeholder { font-size: 28rpx; color: #c0c4cc; flex: 1; }
|
||||||
|
.drawer-body { display: flex; flex-direction: column; max-height: 70vh; }
|
||||||
|
.search-box { padding: 16rpx 24rpx; flex-shrink: 0; }
|
||||||
|
.crumb-scroll { white-space: nowrap; padding: 0 24rpx 16rpx; flex-shrink: 0; }
|
||||||
|
.crumbs { display: inline-flex; flex-wrap: nowrap; }
|
||||||
|
.crumb { font-size: 26rpx; color: #6acdbb; }
|
||||||
|
.crumb.muted { color: #86909c; }
|
||||||
|
.crumb-sep { color: #c0c4cc; }
|
||||||
|
.list-scroll { flex: 1; max-height: 52vh; }
|
||||||
|
.list-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 28rpx 32rpx;
|
||||||
|
border-bottom: 1rpx solid #f5f5f5;
|
||||||
|
}
|
||||||
|
.row-label { font-size: 28rpx; color: #1d2129; flex: 1; }
|
||||||
|
.row-arrow { font-size: 36rpx; color: #c0c4cc; }
|
||||||
|
.empty { text-align: center; color: #86909c; padding: 80rpx 0; font-size: 28rpx; }
|
||||||
|
</style>
|
||||||
141
subPackages/sub_salesperson/components/StoreInputPicker.vue
Normal file
141
subPackages/sub_salesperson/components/StoreInputPicker.vue
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
<template>
|
||||||
|
<view class="picker-wrap">
|
||||||
|
<view class="store-input-picker" @click="openDrawer">
|
||||||
|
<text :class="summary ? 'summary-text' : 'placeholder-text'">{{ summary || placeholder }}</text>
|
||||||
|
<u-icon v-if="!disabled" name="arrow-right" color="#c0c4cc" size="28"></u-icon>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<drawer-page-container v-model="show" title="选择预填门店">
|
||||||
|
<view class="drawer-body">
|
||||||
|
<scroll-view scroll-y class="card-scroll">
|
||||||
|
<view v-if="loading" class="empty">加载中...</view>
|
||||||
|
<view v-else-if="!options.length" class="empty">暂无预填门店</view>
|
||||||
|
<view
|
||||||
|
v-for="item in options"
|
||||||
|
:key="item.id"
|
||||||
|
class="store-card"
|
||||||
|
:class="{ active: innerValue.includes(item.id) }"
|
||||||
|
@click="toggle(item.id)"
|
||||||
|
>
|
||||||
|
<view class="card-head">
|
||||||
|
<text class="card-name">{{ item.name || '-' }}</text>
|
||||||
|
<text class="card-status" :class="'s' + item.status">{{ item.status_text }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="card-mobile">{{ item.mobile || '-' }}</text>
|
||||||
|
<text v-if="innerValue.includes(item.id)" class="card-check">已选</text>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
<view class="drawer-footer">
|
||||||
|
<u-button type="primary" :custom-style="primaryStyle" @click="confirm">确定({{ innerValue.length }})</u-button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</drawer-page-container>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import DrawerPageContainer from '@/subPackages/sub_salesperson/components/DrawerPageContainer.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'StoreInputPicker',
|
||||||
|
components: { DrawerPageContainer },
|
||||||
|
props: {
|
||||||
|
value: { type: Array, default: () => [] },
|
||||||
|
options: { type: Array, default: () => [] },
|
||||||
|
loading: { type: Boolean, default: false },
|
||||||
|
disabled: { type: Boolean, default: false },
|
||||||
|
placeholder: { type: String, default: '请选择预填门店' },
|
||||||
|
defaultStoreInputId: { type: [Number, String], default: 0 },
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
show: false,
|
||||||
|
innerValue: [],
|
||||||
|
primaryStyle: {
|
||||||
|
backgroundColor: '#6acdbb',
|
||||||
|
color: '#ffffff',
|
||||||
|
borderColor: '#6acdbb',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
summary() {
|
||||||
|
if (!this.value.length) return ''
|
||||||
|
const names = this.options
|
||||||
|
.filter((o) => this.value.includes(o.id))
|
||||||
|
.map((o) => o.name)
|
||||||
|
if (names.length) return names.join('、')
|
||||||
|
return `已选 ${this.value.length} 个门店`
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
defaultStoreInputId: {
|
||||||
|
immediate: true,
|
||||||
|
handler(id) {
|
||||||
|
const n = Number(id || 0)
|
||||||
|
if (n > 0 && !this.value.includes(n)) {
|
||||||
|
this.$emit('input', [n])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
openDrawer() {
|
||||||
|
if (this.disabled) return
|
||||||
|
this.innerValue = [...this.value]
|
||||||
|
const preset = Number(this.defaultStoreInputId || 0)
|
||||||
|
if (preset > 0 && !this.innerValue.includes(preset)) {
|
||||||
|
this.innerValue.push(preset)
|
||||||
|
}
|
||||||
|
this.show = true
|
||||||
|
},
|
||||||
|
toggle(id) {
|
||||||
|
const idx = this.innerValue.indexOf(id)
|
||||||
|
if (idx === -1) this.innerValue.push(id)
|
||||||
|
else this.innerValue.splice(idx, 1)
|
||||||
|
},
|
||||||
|
confirm() {
|
||||||
|
if (!this.innerValue.length) {
|
||||||
|
uni.showToast({ title: '请选择预填门店', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$emit('input', [...this.innerValue])
|
||||||
|
this.show = false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.picker-wrap { width: 100%; }
|
||||||
|
.store-input-picker {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 72rpx;
|
||||||
|
}
|
||||||
|
.summary-text { font-size: 28rpx; color: #303133; flex: 1; }
|
||||||
|
.placeholder-text { font-size: 28rpx; color: #c0c4cc; flex: 1; }
|
||||||
|
.drawer-body { display: flex; flex-direction: column; max-height: 70vh; }
|
||||||
|
.card-scroll { flex: 1; max-height: 58vh; padding: 16rpx 24rpx; box-sizing: border-box; }
|
||||||
|
.store-card {
|
||||||
|
background: #f7f8fa;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 24rpx;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
border: 2rpx solid transparent;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.store-card.active { border-color: #6acdbb; background: rgba(106, 205, 187, 0.08); }
|
||||||
|
.card-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8rpx; }
|
||||||
|
.card-name { font-size: 30rpx; font-weight: 600; color: #1d2129; flex: 1; }
|
||||||
|
.card-status { font-size: 22rpx; padding: 4rpx 12rpx; border-radius: 8rpx; }
|
||||||
|
.s0 { color: #ff7d00; background: rgba(255, 125, 0, 0.1); }
|
||||||
|
.s1 { color: #00b42a; background: rgba(0, 180, 42, 0.1); }
|
||||||
|
.s2 { color: #f53f3f; background: rgba(245, 63, 63, 0.1); }
|
||||||
|
.card-mobile { font-size: 26rpx; color: #86909c; }
|
||||||
|
.card-check { position: absolute; right: 24rpx; bottom: 24rpx; font-size: 24rpx; color: #6acdbb; font-weight: 600; }
|
||||||
|
.empty { text-align: center; color: #86909c; padding: 80rpx 0; font-size: 28rpx; }
|
||||||
|
.drawer-footer { padding: 16rpx 24rpx 32rpx; flex-shrink: 0; }
|
||||||
|
</style>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user