feat: 会员管理、在线复诊优化、新增配送仓库

This commit is contained in:
李琦
2026-07-23 09:25:00 +08:00
parent b0e9dfe2a3
commit dabfa45ac0
9 changed files with 176 additions and 48 deletions

View File

@@ -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' },
],
};
}

View File

@@ -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 || '—';

View File

@@ -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">

View File

@@ -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>

View File

@@ -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) {

View File

@@ -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 },
],
};
}

View 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: '提交异常,请稍后重试' })
}
}

View File

@@ -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' })
},
})
},
},

View File

@@ -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
},
// --- 最终提交 ---
// --- 最终提交(点击即 loading10s 未发请求或异常则收起并提示) ---
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() {