feat: 会员管理、在线复诊优化、新增配送仓库
This commit is contained in:
@@ -29,8 +29,8 @@ export function getPlatformAdminContext() {
|
||||
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 },
|
||||
{ text: '工作台', iconPath: 'home', selectedIconPath: 'home-fill', pagePath: PLATFORM_HOME },
|
||||
{ text: '我的', iconPath: 'account', selectedIconPath: 'account-fill', pagePath: PLATFORM_MY },
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -53,8 +53,8 @@ export function getSalespersonContext() {
|
||||
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 },
|
||||
{ text: '工作台', iconPath: 'home', selectedIconPath: 'home-fill', pagePath: SALESPERSON_HOME },
|
||||
{ text: '我的', iconPath: 'account', selectedIconPath: 'account-fill', pagePath: SALESPERSON_MY },
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -79,8 +79,8 @@ export function getClinicAdminContext() {
|
||||
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' },
|
||||
{ text: '工作台', iconPath: 'home', selectedIconPath: 'home-fill', pagePath: '/subPackages/sub_clinic_admin/home/index' },
|
||||
{ text: '我的', iconPath: 'account', selectedIconPath: 'account-fill', pagePath: '/subPackages/sub_clinic_admin/my/index' },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ export default {
|
||||
return this.user.nickname || '—';
|
||||
},
|
||||
userAvatar() {
|
||||
return this.user.avatarurl || '/static/image/default-avatar.png';
|
||||
return this.user.avatarurl || 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/mine/mr_tx.png';
|
||||
},
|
||||
patientName() {
|
||||
if (typeof this.patient === 'string') return this.patient || '—';
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<view class="profile-row">
|
||||
<image
|
||||
class="avatar"
|
||||
:src="profile.user.avatarurl || '/static/image/default-avatar.png'"
|
||||
:src="profile.user.avatarurl || 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/mine/mr_tx.png'"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view class="meta">
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<!-- 顶部:用户 + 就诊人分区卡片(固定,不进 swiper) -->
|
||||
<view class="profile-card">
|
||||
<view class="profile-row">
|
||||
<image class="avatar" :src="profile.user.avatarurl || '/static/image/default-avatar.png'" mode="aspectFill" />
|
||||
<image class="avatar" :src="profile.user.avatarurl || 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/mine/mr_tx.png'" mode="aspectFill" />
|
||||
<view class="meta">
|
||||
<text class="name">{{ profile.user.nickname || '微信用户' }}</text>
|
||||
<text class="sub">ID {{ profile.user.id || '—' }}<template v-if="profile.user.mobile"> · {{ profile.user.mobile }}</template></text>
|
||||
|
||||
@@ -68,7 +68,7 @@ export default {
|
||||
methods: {
|
||||
avatarSrc(raw) {
|
||||
const s = String(raw || '').trim();
|
||||
return s || '/static/image/default-avatar.png';
|
||||
return s || 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/mine/mr_tx.png';
|
||||
},
|
||||
/** 标签文案:姓名 或 姓名 · 脱敏手机 */
|
||||
patientTagText(p) {
|
||||
|
||||
@@ -28,8 +28,8 @@ export function getPlatformAdminContext() {
|
||||
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 },
|
||||
{ text: '工作台', iconPath: 'home', selectedIconPath: 'home-fill', pagePath: PLATFORM_HOME },
|
||||
{ text: '我的', iconPath: 'account', selectedIconPath: 'account-fill', pagePath: PLATFORM_MY },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
115
subPackages/sub_salesperson/common/submitGuard.js
Normal file
115
subPackages/sub_salesperson/common/submitGuard.js
Normal file
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* 表单提交 loading / 防重复点击 / 「未发起请求」超时守卫。
|
||||
* 为什么抽成公共模块:门店录入与医生录入提交流程一致,避免两页各自维护一套定时器与异常收尾逻辑。
|
||||
*/
|
||||
|
||||
/** 点击后若超过该毫秒仍未真正发起请求,则取消 loading 并提示 */
|
||||
export const SUBMIT_PREP_TIMEOUT_MS = 10000
|
||||
|
||||
/**
|
||||
* 清理「未发起请求」倒计时,避免页面销毁或重复 begin 时残留定时器。
|
||||
* @param {object} ctx 页面/组件实例(需挂 _submitPrepTimer)
|
||||
*/
|
||||
function clearPrepTimer(ctx) {
|
||||
if (ctx && ctx._submitPrepTimer) {
|
||||
clearTimeout(ctx._submitPrepTimer)
|
||||
ctx._submitPrepTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始提交:立刻 loading + 防重复 + 启动 10s「未发起请求」倒计时。
|
||||
* @param {object} ctx 需有 data.submitting
|
||||
* @param {{ title?: string }} [options]
|
||||
* @returns {boolean} false 表示已在提交中,调用方应直接 return
|
||||
*/
|
||||
export function beginSubmit(ctx, options = {}) {
|
||||
if (!ctx || ctx.submitting) return false
|
||||
ctx.submitting = true
|
||||
ctx._submitRequestStarted = false
|
||||
clearPrepTimer(ctx)
|
||||
uni.showLoading({ title: options.title || '提交中...', mask: true })
|
||||
// 10 秒内若仍未 markRequestStarted,说明卡在校验/组 payload,需收起 loading 并友好提示
|
||||
ctx._submitPrepTimer = setTimeout(() => {
|
||||
ctx._submitPrepTimer = null
|
||||
if (ctx._submitRequestStarted) return
|
||||
endSubmit(ctx, { toast: '提交超时,请稍后重试' })
|
||||
}, SUBMIT_PREP_TIMEOUT_MS)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记已真正发起 API 请求:清除「未发起请求」倒计时,loading 继续等待接口返回。
|
||||
* @param {object} ctx
|
||||
*/
|
||||
export function markRequestStarted(ctx) {
|
||||
if (!ctx) return
|
||||
ctx._submitRequestStarted = true
|
||||
clearPrepTimer(ctx)
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束提交:清定时器、复位 submitting;默认关 loading,可选 toast。
|
||||
* 校验失败时 showError 已 showToast(会顶掉 loading),须传 skipHideLoading,否则 hideLoading 会把 toast 清掉。
|
||||
* @param {object} ctx
|
||||
* @param {{ toast?: string, skipHideLoading?: boolean }} [options]
|
||||
*/
|
||||
export function endSubmit(ctx, options = {}) {
|
||||
if (!ctx) return
|
||||
clearPrepTimer(ctx)
|
||||
ctx._submitRequestStarted = false
|
||||
ctx.submitting = false
|
||||
if (!options.skipHideLoading) {
|
||||
try {
|
||||
uni.hideLoading()
|
||||
} catch (e) {
|
||||
// hideLoading 在无 loading 时偶发抛错,忽略以免二次异常
|
||||
}
|
||||
}
|
||||
if (options.toast) {
|
||||
uni.showToast({ title: options.toast, icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一跑完「校验 → 发请求 → 成功/失败/异常收尾」。
|
||||
* @param {object} ctx
|
||||
* @param {{
|
||||
* validate: () => boolean,
|
||||
* request: () => Promise<{ ok?: boolean, res?: any, msg?: string, data?: any }>,
|
||||
* onSuccess: (w: any) => void,
|
||||
* loadingTitle?: string,
|
||||
* }} options
|
||||
*/
|
||||
export function runSubmit(ctx, options) {
|
||||
const { validate, request, onSuccess, loadingTitle } = options || {}
|
||||
if (!beginSubmit(ctx, { title: loadingTitle })) return
|
||||
try {
|
||||
if (typeof validate === 'function' && !validate()) {
|
||||
// 校验失败:showError 已 toast(顶掉 loading),只复位状态,勿再 hideLoading
|
||||
endSubmit(ctx, { skipHideLoading: true })
|
||||
return
|
||||
}
|
||||
markRequestStarted(ctx)
|
||||
const reqPromise = typeof request === 'function' ? request() : null
|
||||
if (!reqPromise || typeof reqPromise.then !== 'function') {
|
||||
endSubmit(ctx, { toast: '提交异常,请稍后重试' })
|
||||
return
|
||||
}
|
||||
reqPromise
|
||||
.then((w) => {
|
||||
if (w && w.ok) {
|
||||
endSubmit(ctx)
|
||||
if (typeof onSuccess === 'function') onSuccess(w)
|
||||
return
|
||||
}
|
||||
const msg = (w && w.res && w.res.msg) || (w && w.msg) || '提交失败'
|
||||
endSubmit(ctx, { toast: msg })
|
||||
})
|
||||
.catch(() => {
|
||||
endSubmit(ctx, { toast: '提交异常,请稍后重试' })
|
||||
})
|
||||
} catch (e) {
|
||||
endSubmit(ctx, { toast: '提交异常,请稍后重试' })
|
||||
}
|
||||
}
|
||||
@@ -147,7 +147,7 @@
|
||||
<u-button v-if="canSaveDraft" class="btn-draft" @click="openSaveDraft">存草稿</u-button>
|
||||
<u-button v-if="currentStep > 0" class="btn-prev" @click="prevStep">上一步</u-button>
|
||||
<u-button v-if="currentStep < 2" class="btn-next" type="primary" :custom-style="primaryStyle" @click="nextStep">下一步</u-button>
|
||||
<u-button v-if="currentStep === 2" class="btn-submit" type="primary" :custom-style="primaryStyle" @click="submit">{{ id ? '确认保存' : '提交预填' }}</u-button>
|
||||
<u-button v-if="currentStep === 2" class="btn-submit" type="primary" :custom-style="primaryStyle" :loading="submitting" :disabled="submitting" @click="submit">{{ id ? '确认保存' : '提交预填' }}</u-button>
|
||||
</view>
|
||||
|
||||
<u-popup v-model="showDraftNameModal" mode="center" border-radius="16" width="600rpx">
|
||||
@@ -180,6 +180,7 @@ import {
|
||||
} from '@/subPackages/sub_salesperson/common/inputFormHelpers.js'
|
||||
import { getDraft, saveDraft, removeDraft } from '@/subPackages/sub_salesperson/common/inputDraftStorage.js'
|
||||
import { serializeDoctorDraft, applyDoctorDraft } from '@/subPackages/sub_salesperson/common/inputDraftHelpers.js'
|
||||
import { runSubmit, endSubmit } from '@/subPackages/sub_salesperson/common/submitGuard.js'
|
||||
|
||||
export default {
|
||||
mixins: [businessMixin],
|
||||
@@ -190,6 +191,8 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
id: 0,
|
||||
// 提交中:用于按钮 loading/禁用,配合 submitGuard 防重复点击
|
||||
submitting: false,
|
||||
currentDraftId: '',
|
||||
showDraftNameModal: false,
|
||||
draftNameInput: '',
|
||||
@@ -302,6 +305,10 @@ export default {
|
||||
}
|
||||
})
|
||||
},
|
||||
// 页面销毁时收起提交 loading,避免定时器残留
|
||||
beforeDestroy() {
|
||||
endSubmit(this)
|
||||
},
|
||||
methods: {
|
||||
pickSelectRow(e) {
|
||||
if (Array.isArray(e)) return e[0] || null
|
||||
@@ -483,28 +490,28 @@ export default {
|
||||
return true
|
||||
},
|
||||
submit() {
|
||||
if (!this.validate()) return
|
||||
const payload = {
|
||||
...this.form,
|
||||
register_price: Number(this.form.register_price) || 0,
|
||||
store_input_ids: this.selectedStoreInputIds,
|
||||
store_ids: this.selectedStoreIds,
|
||||
store_id: this.selectedStoreInputIds[0] || this.selectedStoreIds[0] || 0,
|
||||
}
|
||||
const req = this.id
|
||||
? this.bizApi.updateDoctorInput({ ...payload, id: this.id })
|
||||
: this.bizApi.createDoctorInput(payload)
|
||||
req.then((w) => {
|
||||
if (w.ok) {
|
||||
runSubmit(this, {
|
||||
validate: () => this.validate(),
|
||||
request: () => {
|
||||
const payload = {
|
||||
...this.form,
|
||||
register_price: Number(this.form.register_price) || 0,
|
||||
store_input_ids: this.selectedStoreInputIds,
|
||||
store_ids: this.selectedStoreIds,
|
||||
store_id: this.selectedStoreInputIds[0] || this.selectedStoreIds[0] || 0,
|
||||
}
|
||||
return this.id
|
||||
? this.bizApi.updateDoctorInput({ ...payload, id: this.id })
|
||||
: this.bizApi.createDoctorInput(payload)
|
||||
},
|
||||
onSuccess: () => {
|
||||
if (this.currentDraftId) {
|
||||
removeDraft('doctor', this.currentDraftId)
|
||||
this.currentDraftId = ''
|
||||
}
|
||||
uni.showToast({ title: '已保存' })
|
||||
setTimeout(() => uni.navigateBack(), 500)
|
||||
return
|
||||
}
|
||||
uni.showToast({ title: (w.res && w.res.msg) || w.msg || '提交失败', icon: 'none' })
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
@@ -176,7 +176,7 @@
|
||||
<u-button v-if="canSaveDraft" class="btn-draft" @click="openSaveDraft">存草稿</u-button>
|
||||
<u-button v-if="currentStep > 0" class="btn-prev" @click="prevStep">上一步</u-button>
|
||||
<u-button v-if="currentStep < 2" class="btn-next" type="primary" :custom-style="primaryStyle" @click="nextStep">下一步</u-button>
|
||||
<u-button v-if="currentStep === 2" class="btn-submit" type="primary" :custom-style="primaryStyle" @click="submit">确认保存</u-button>
|
||||
<u-button v-if="currentStep === 2" class="btn-submit" type="primary" :custom-style="primaryStyle" :loading="submitting" :disabled="submitting" @click="submit">确认保存</u-button>
|
||||
</view>
|
||||
|
||||
<u-popup v-model="showDraftNameModal" mode="center" border-radius="16" width="600rpx">
|
||||
@@ -202,6 +202,7 @@ import { pickAndUploadImage, openGalleryPick, createDefaultStoreInputForm } from
|
||||
import { getDraft, saveDraft, removeDraft } from '@/subPackages/sub_salesperson/common/inputDraftStorage.js'
|
||||
import { serializeStoreDraft, applyStoreDraft } from '@/subPackages/sub_salesperson/common/inputDraftHelpers.js'
|
||||
import { goDoctorInputFromStore } from '@/subPackages/sub_salesperson/common/inputNavigation.js'
|
||||
import { runSubmit, endSubmit } from '@/subPackages/sub_salesperson/common/submitGuard.js'
|
||||
|
||||
export default {
|
||||
mixins: [businessMixin],
|
||||
@@ -212,6 +213,8 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
id: 0,
|
||||
// 提交中:用于按钮 loading/禁用,配合 submitGuard 防重复点击
|
||||
submitting: false,
|
||||
submitSuccess: false,
|
||||
lastCreatedStoreInputId: 0,
|
||||
currentDraftId: '',
|
||||
@@ -289,6 +292,10 @@ export default {
|
||||
}
|
||||
this.loadPromoterOptions()
|
||||
},
|
||||
// 页面销毁时收起提交 loading,避免定时器残留
|
||||
beforeDestroy() {
|
||||
endSubmit(this)
|
||||
},
|
||||
methods: {
|
||||
loadPromoterOptions() {
|
||||
this.promoterLoading = true
|
||||
@@ -457,24 +464,23 @@ export default {
|
||||
return true
|
||||
},
|
||||
|
||||
// --- 最终提交 ---
|
||||
// --- 最终提交(点击即 loading;10s 未发请求或异常则收起并提示) ---
|
||||
submit() {
|
||||
if (!this.validateOptionalBankFields()) return
|
||||
|
||||
const payload = {
|
||||
...this.form,
|
||||
type: Number(this.form.type) === 1 ? 1 : 0,
|
||||
address: [Number(this.form.province_id), Number(this.form.city_id)],
|
||||
}
|
||||
delete payload.erp_id
|
||||
delete payload.mes_id
|
||||
|
||||
const req = this.id
|
||||
? this.bizApi.updateStoreInput({ ...payload, id: this.id })
|
||||
: this.bizApi.createStoreInput(payload)
|
||||
|
||||
req.then(w => {
|
||||
if (w.ok) {
|
||||
runSubmit(this, {
|
||||
validate: () => this.validateOptionalBankFields(),
|
||||
request: () => {
|
||||
const payload = {
|
||||
...this.form,
|
||||
type: Number(this.form.type) === 1 ? 1 : 0,
|
||||
address: [Number(this.form.province_id), Number(this.form.city_id)],
|
||||
}
|
||||
delete payload.erp_id
|
||||
delete payload.mes_id
|
||||
return this.id
|
||||
? this.bizApi.updateStoreInput({ ...payload, id: this.id })
|
||||
: this.bizApi.createStoreInput(payload)
|
||||
},
|
||||
onSuccess: (w) => {
|
||||
if (this.currentDraftId) {
|
||||
removeDraft('store', this.currentDraftId)
|
||||
this.currentDraftId = ''
|
||||
@@ -488,7 +494,7 @@ export default {
|
||||
this.lastCreatedStoreInputId = Number(newId)
|
||||
this.submitSuccess = true
|
||||
uni.showToast({ title: '提交成功', icon: 'success' })
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
goDoctorInput() {
|
||||
|
||||
Reference in New Issue
Block a user