Files
xk-doctor-wx/subPackages/sub_salesperson/store-input/form.vue
2026-06-18 15:05:14 +08:00

587 lines
24 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<business-page-layout :title="pageTitle" :show-back="true">
<!-- 顶部步骤条 -->
<view v-if="!submitSuccess" class="step-header">
<u-steps :list="stepList" :current="currentStep" active-color="#6acdbb"></u-steps>
</view>
<scroll-view v-if="!submitSuccess" scroll-y class="form-scroll">
<u-form :model="form" ref="uForm" label-width="220" label-align="left">
<!-- ================= 步骤 1基础与地址 ================= -->
<view v-show="currentStep === 0" class="step-content">
<view class="section-title">基础信息</view>
<u-form-item label="门店名称" :required="true">
<u-input v-model="form.name" placeholder="请输入门店名称" />
</u-form-item>
<u-form-item label="门店类型" right-icon="arrow-right" :required="true">
<u-input :value="storeTypeLabel" type="select" @click="showStoreType = true" placeholder="请选择" />
<u-select v-model="showStoreType" :list="storeTypes" @confirm="onStoreTypeConfirm"></u-select>
</u-form-item>
<u-form-item label="联系人姓名" :required="true">
<u-input v-model="form.contact" placeholder="请输入联系人姓名" />
</u-form-item>
<u-form-item label="手机号" :required="true">
<u-input v-model="form.mobile" type="number" placeholder="请输入联系人手机号" />
</u-form-item>
<u-form-item label="业务员" :required="true" class="form-item-promoter" :border-bottom="false">
<promoter-picker
:value="form.code"
:options="promoterOptions"
:loading="promoterLoading"
:disabled="promoterDisabled"
@input="form.code = $event"
/>
</u-form-item>
<u-form-item label="诊所类型" :border-bottom="false">
<view class="clinic-type-cards">
<view
v-for="(item, idx) in clinicTypes"
:key="item.value"
class="clinic-type-card"
:class="{ active: clinicTypeIndex === idx }"
@click="selectClinicType(idx)"
>
<text class="clinic-type-label">{{ item.label }}</text>
</view>
</view>
</u-form-item>
<u-form-item label="是否包邮">
<view class="switch-row">
<text class="switch-hint">{{ Number(form.is_shipping_free) === 1 ? '包邮' : '不包邮' }}</text>
<switch
:checked="Number(form.is_shipping_free) === 1"
color="#6acdbb"
@change="onShippingFreeChange"
/>
</view>
</u-form-item>
<u-form-item label="订阅价格波动">
<view class="switch-row">
<text class="switch-hint">{{ Number(form.subscribe_price_change) === 0 ? '订阅' : '不订阅' }}</text>
<switch
:checked="Number(form.subscribe_price_change) === 0"
color="#6acdbb"
@change="onSubscribeChange"
/>
</view>
</u-form-item>
<u-form-item label="营业时间" right-icon="arrow-right">
<view class="time-flex">
<view class="time-box" @click="showStartTime = true">{{ form.start_time || '开始时间' }}</view>
<text class="time-sep">-</text>
<view class="time-box" @click="showEndTime = true">{{ form.end_time || '结束时间' }}</view>
</view>
<u-picker mode="time" v-model="showStartTime" :params="{ hour: true, minute: true }" @confirm="e => onTimeConfirm('start_time', e)"></u-picker>
<u-picker mode="time" v-model="showEndTime" :params="{ hour: true, minute: true }" @confirm="e => onTimeConfirm('end_time', e)"></u-picker>
</u-form-item>
<view class="section-title">地址信息</view>
<u-form-item label="省市区" :required="true">
<region-address-picker :value="regionValue" @change="onRegionChange" />
</u-form-item>
<u-form-item label="详细地址" :required="true">
<u-input v-model="form.position" placeholder="请输入详细地址" />
</u-form-item>
</view>
<!-- ================= 步骤 2资料与比例 ================= -->
<view v-show="currentStep === 1" class="step-content">
<view class="section-title">图片资料</view>
<view class="upload-group">
<view class="upload-label">公章</view>
<view class="img-row">
<image v-if="form.see_rate" :src="form.see_rate" class="thumb" mode="aspectFill" @click="uploadField('see_rate')" />
<view v-else class="upload-btn" @click="uploadField('see_rate')">
<u-icon name="plus" size="40" color="#999"></u-icon>
<text class="upload-txt">上传公章</text>
</view>
</view>
<text class="gallery-link" @click="pickFromGallery('see_rate')">从图库选择</text>
</view>
<view class="upload-group">
<view class="upload-label"><text class="required">*</text>轮播图</view>
<view class="img-list">
<image v-for="(u, i) in form.url" :key="i" :src="u" class="thumb" mode="aspectFill" />
<view class="upload-btn" @click="uploadCarousel">
<u-icon name="plus" size="40" color="#999"></u-icon>
</view>
</view>
<text class="gallery-link" @click="pickCarouselFromGallery">从图库选择</text>
</view>
<view class="upload-group">
<view class="upload-label">合同文件 (已传 {{ form.contract_files.length }} )</view>
<view class="upload-btn contract-btn" @click="uploadContract">
<u-icon name="file-text" size="40" color="#6acdbb"></u-icon>
<text class="upload-txt" style="color: #6acdbb;">添加合同</text>
</view>
</view>
<view class="section-title">销售比例</view>
<u-form-item label="中药采购比例(%)">
<u-input v-model.number="form.z_buy_percent" type="digit" placeholder="请输入比例" />
</u-form-item>
<u-form-item label="中药销售比例(%)">
<u-input v-model.number="form.z_sale_percent" type="digit" placeholder="请输入比例" />
</u-form-item>
</view>
<!-- ================= 步骤 3结算信息可选 ================= -->
<view v-show="currentStep === 2" class="step-content">
<view class="section-title">银行卡信息</view>
<view class="step-tip">可选填写也可在审核通过后于详情页单独报备</view>
<u-form-item label="账户类型" right-icon="arrow-right">
<u-input :value="bankTypes[bankTypeIndex].label" type="select" @click="showBankType = true" placeholder="请选择" />
<u-select v-model="showBankType" :list="bankTypes" @confirm="onBankTypeConfirm"></u-select>
</u-form-item>
<u-form-item label="开户人姓名">
<u-input v-model="form.bank_user_name" placeholder="请输入开户人姓名" />
</u-form-item>
<u-form-item label="银行卡号">
<u-input v-model="form.bank_card" type="number" placeholder="请输入银行卡号" />
</u-form-item>
<u-form-item label="开户行">
<u-input v-model="form.bank_name" placeholder="请输入开户行名称 (如: 招商银行)" />
</u-form-item>
<u-form-item label="银联行号">
<u-input v-model="form.bank_no" type="number" placeholder="选填,请输入行号" />
</u-form-item>
</view>
</u-form>
</scroll-view>
<view v-if="submitSuccess" class="success-panel">
<u-icon name="checkmark-circle-fill" color="#6acdbb" size="120"></u-icon>
<text class="success-title">提交成功</text>
<text class="success-sub">{{ form.name || '门店' }} 已提交等待审核</text>
<view class="success-actions">
<u-button type="primary" :custom-style="primaryStyle" @click="goDoctorInput">录入医生</u-button>
<u-button @click="goBackList">返回列表</u-button>
</view>
</view>
<!-- 底部操作按钮 -->
<view v-if="!submitSuccess" class="footer-actions">
<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>
</view>
<u-popup v-model="showDraftNameModal" mode="center" border-radius="16" width="600rpx">
<view class="draft-name-box">
<text class="draft-name-title">草稿名称</text>
<u-input v-model="draftNameInput" placeholder="请输入草稿名称" />
<view class="draft-name-actions">
<u-button size="medium" @click="showDraftNameModal = false">取消</u-button>
<u-button size="medium" type="primary" :custom-style="primaryStyle" @click="confirmSaveDraft">保存</u-button>
</view>
</view>
</u-popup>
</business-page-layout>
</template>
<script>
import BusinessPageLayout from '@/subPackages/sub_salesperson/components/business-page-layout.vue'
import RegionAddressPicker from '@/subPackages/sub_salesperson/components/RegionAddressPicker.vue'
import PromoterPicker from '@/subPackages/sub_salesperson/components/PromoterPicker.vue'
import businessMixin from '@/subPackages/sub_salesperson/common/businessMixin.js'
import { getInputBusinessContext } from '@/subPackages/sub_salesperson/common/context.js'
import { pickAndUploadImage, openGalleryPick, createDefaultStoreInputForm } from '@/subPackages/sub_salesperson/common/inputFormHelpers.js'
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'
export default {
mixins: [businessMixin],
components: { BusinessPageLayout, RegionAddressPicker, PromoterPicker },
provide() {
return { businessContext: getInputBusinessContext() }
},
data() {
return {
id: 0,
submitSuccess: false,
lastCreatedStoreInputId: 0,
currentDraftId: '',
showDraftNameModal: false,
draftNameInput: '',
// 分步表单状态
currentStep: 0,
stepList: [
{ name: '基础与地址' },
{ name: '资料与比例' },
{ name: '结算信息' }
],
// 样式
primaryStyle: {
backgroundColor: '#6acdbb',
color: '#ffffff',
borderColor: '#6acdbb'
},
// uView select/picker 显示状态控制
showStoreType: false,
showStartTime: false,
showEndTime: false,
showBankType: false,
clinicTypeIndex: 0,
bankTypeIndex: 0,
// uView 格式的下拉数据
storeTypes: [{ label: '诊所', value: 0 }, { label: '药店', value: 1 }],
storeTypeIndex: 0,
clinicTypes: [{ label: '西医诊所', value: 1 }, { label: '中医诊所', value: 2 }],
bankTypes: [{ label: '对公', value: 1 }, { label: '对私', value: 2 }, { label: '存折', value: 5 }],
form: createDefaultStoreInputForm(),
promoterOptions: [],
promoterLoading: false,
promoterDisabled: false,
}
},
computed: {
pageTitle() { return this.id ? '编辑门店录入' : '新建门店录入' },
storeTypeLabel() {
const item = this.storeTypes.find((t) => t.value === Number(this.form.type))
return item ? item.label : '请选择'
},
canSaveDraft() { return !this.id },
regionValue() {
const p = this.form.province_id
const c = this.form.city_id
if (!p || !c) return []
return [Number(p), Number(c)]
},
},
onLoad(q) {
this.id = Number(q.id || 0)
const draftId = q.draftId || ''
if (draftId) {
this.currentDraftId = draftId
}
if (this.id) {
Promise.all([
this.loadPromoterOptions(),
this.loadDetailAsync(),
])
return
}
if (draftId) {
this.loadPromoterOptions().then(() => {
this.loadDraft(draftId)
})
return
}
this.loadPromoterOptions()
},
methods: {
loadPromoterOptions() {
this.promoterLoading = true
return this.bizApi.getPromoterOptions().then((w) => {
this.promoterLoading = false
if (!w.ok) return
this.promoterOptions = Array.isArray(w.data) ? w.data : []
if (!this.id && !this.form.code) {
const userKey = this.bizCtx.userStorageKey || 'salesperson_user'
const user = uni.getStorageSync(userKey) || {}
const roleId = Number(user.role_id || (user.roles && user.roles.id) || 0)
if (roleId === 6) {
const self = this.promoterOptions.find((p) => Number(p.id) === Number(user.id))
if (self) {
this.form.code = self.code
this.promoterDisabled = true
}
}
}
}).catch(() => { this.promoterLoading = false })
},
loadDetailAsync() {
return this.bizApi.getStoreInputDetail(this.id).then((w) => {
if (!w.ok || !w.data) return
const d = w.data
Object.assign(this.form, createDefaultStoreInputForm(), d)
this.form.url = Array.isArray(d.url) ? d.url : (d.pic ? [d.pic] : [])
this.form.contract_files = d.contract_files || []
this.form.province_id = d.province_id || null
this.form.city_id = d.city_id || null
this.form.address = d.province_id && d.city_id ? [d.province_id, d.city_id] : []
this.storeTypeIndex = Number(d.type) === 1 ? 1 : 0
this.clinicTypeIndex = Number(d.clinic_type) === 1 ? 0 : 1
this.bankTypeIndex = Math.max(0, this.bankTypes.findIndex((b) => b.value === Number(d.bank_account_type)))
})
},
selectClinicType(idx) {
this.clinicTypeIndex = idx
this.form.clinic_type = this.clinicTypes[idx].value
},
onShippingFreeChange(e) {
this.form.is_shipping_free = e.detail.value ? 1 : 0
},
onSubscribeChange(e) {
this.form.subscribe_price_change = e.detail.value ? 0 : 1
},
// --- 下拉及选择器确认事件 ---
onStoreTypeConfirm(e) {
const selected = e[0]
this.form.type = selected.value
this.storeTypeIndex = this.storeTypes.findIndex((item) => item.value === selected.value)
},
onBankTypeConfirm(e) {
const selected = e[0];
this.form.bank_account_type = selected.value;
this.bankTypeIndex = this.bankTypes.findIndex(item => item.value === selected.value);
},
onTimeConfirm(field, e) {
this.form[field] = `${e.hour}:${e.minute}`;
},
onRegionChange(val) {
if (!val || val.length < 2) return
this.form.province_id = val[0]
this.form.city_id = val[1]
this.form.address = [val[0], val[1]]
},
// --- 图片上传相关 ---
uploadField(field) {
pickAndUploadImage().then(url => { this.form[field] = url }).catch(() => {})
},
pickFromGallery(field) {
openGalleryPick().then(url => { this.form[field] = url }).catch(() => {})
},
pickCarouselFromGallery() {
openGalleryPick().then(url => { this.form.url.push(url) }).catch(() => {})
},
uploadCarousel() {
pickAndUploadImage().then(url => { this.form.url.push(url) }).catch(() => {})
},
uploadContract() {
pickAndUploadImage().then(url => {
this.form.contract_files.push({ url, name: 'contract' })
}).catch(() => {})
},
loadDraft(draftId) {
const draft = getDraft('store', draftId)
if (!draft) {
uni.showToast({ title: '草稿不存在', icon: 'none' })
this.currentDraftId = ''
return
}
applyStoreDraft(this, draft)
},
openSaveDraft() {
const defaultName = (this.form.name || '').trim() || '未命名草稿'
this.draftNameInput = defaultName
this.showDraftNameModal = true
},
confirmSaveDraft() {
const name = (this.draftNameInput || '').trim()
if (!name) {
uni.showToast({ title: '请输入草稿名称', icon: 'none' })
return
}
try {
const { form, ui } = serializeStoreDraft(this)
const saved = saveDraft('store', {
name,
form,
ui,
draftId: this.currentDraftId || undefined,
})
this.currentDraftId = saved.id
this.showDraftNameModal = false
uni.showToast({ title: '草稿已保存', icon: 'success' })
} catch (e) {
uni.showToast({ title: e.message || '保存失败', icon: 'none' })
}
},
// --- 分步与验证逻辑 ---
nextStep() {
if (this.validateCurrentStep()) {
this.currentStep += 1;
}
},
prevStep() {
this.currentStep -= 1;
},
validateCurrentStep() {
const f = this.form;
// 第1步校验
if (this.currentStep === 0) {
if (!f.name?.trim()) return this.showError('请输入门店名称');
if (f.type !== 0 && f.type !== 1) return this.showError('请选择门店类型');
if (!f.contact?.trim()) return this.showError('请输入联系人姓名');
if (!f.mobile?.trim()) return this.showError('请输入手机号');
if (!f.code?.trim()) return this.showError('请选择业务员');
if (!f.province_id || !f.city_id) return this.showError('请选择省市区');
if (!f.position?.trim()) return this.showError('请输入详细地址');
}
// 第2步校验
if (this.currentStep === 1) {
if (!f.url.length) return this.showError('请至少上传一张轮播图');
}
return true;
},
showError(msg) {
uni.showToast({ title: msg, icon: 'none' });
return false;
},
validateOptionalBankFields() {
const f = this.form
const hasAny = !!(f.bank_user_name?.trim() || f.bank_card?.trim() || f.bank_name?.trim() || f.bank_no?.trim())
if (!hasAny) return true
if (!f.bank_user_name?.trim()) return this.showError('填写银行卡时请填写开户人姓名')
if (!f.bank_card?.trim()) return this.showError('填写银行卡时请填写卡号')
if (!f.bank_name?.trim()) return this.showError('填写银行卡时请填写开户行')
return true
},
// --- 最终提交 ---
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) {
if (this.currentDraftId) {
removeDraft('store', this.currentDraftId)
this.currentDraftId = ''
}
if (this.id) {
uni.showToast({ title: '已保存', icon: 'success' })
setTimeout(() => uni.navigateBack(), 500)
return
}
const newId = (w.data && w.data.id) || 0
this.lastCreatedStoreInputId = Number(newId)
this.submitSuccess = true
uni.showToast({ title: '提交成功', icon: 'success' })
}
})
},
goDoctorInput() {
goDoctorInputFromStore(this.lastCreatedStoreInputId, this.bizCtx)
},
goBackList() {
uni.navigateBack()
},
},
}
</script>
<style scoped>
.form-item-promoter ::v-deep .u-form-item__body {
overflow: visible;
}
.form-item-promoter ::v-deep .u-form-item--left__content__slot {
flex: 1;
min-width: 0;
}
/* 容器及结构 */
.step-header { background-color: #fff; padding: 30rpx 40rpx; margin-bottom: 20rpx; }
.form-scroll { height: calc(100vh - 480rpx); box-sizing: border-box; }
.step-content { background: #fff; padding: 0 32rpx; border-radius: 16rpx; margin: 0 24rpx; }
/* 标题 */
.section-title { font-size: 32rpx; font-weight: 600; padding: 40rpx 0 20rpx; color: #1D2129; position: relative; }
.step-tip { font-size: 24rpx; color: #86909c; margin: -8rpx 0 16rpx; line-height: 1.5; }
.section-title::after { content: ''; position: absolute; left: -32rpx; top: 46rpx; width: 8rpx; height: 32rpx; background: #6acdbb; border-radius: 0 4rpx 4rpx 0; }
/* 营业时间特别处理 */
.time-flex { display: flex; align-items: center; justify-content: space-between; width: 100%; }
.time-box { flex: 1; text-align: center; background: #f5f7fa; padding: 12rpx 0; border-radius: 8rpx; font-size: 28rpx; color: #606266; }
.time-sep { margin: 0 20rpx; color: #999; }
.clinic-type-cards { display: flex; gap: 12rpx; width: 100%; }
.clinic-type-card {
flex: 1;
padding: 12rpx 10rpx;
text-align: center;
background: #f5f7fa;
border: 2rpx solid #e4e7ed;
border-radius: 8rpx;
box-sizing: border-box;
}
.clinic-type-card.active {
background: rgba(106, 205, 187, 0.08);
border-color: #6acdbb;
}
.clinic-type-label { font-size: 24rpx; color: #303133; }
.clinic-type-card.active .clinic-type-label { color: #6acdbb; font-weight: 600; }
.switch-row {
display: flex;
align-items: center;
justify-content: flex-end;
width: 100%;
gap: 16rpx;
}
.switch-hint { font-size: 26rpx; color: #86909c; }
/* 图片资料区域 */
.upload-group { margin-bottom: 30rpx; padding-bottom: 20rpx; border-bottom: 1rpx solid #f5f5f5; }
.upload-group:last-child { border-bottom: none; }
.upload-label { font-size: 28rpx; color: #303133; margin-bottom: 20rpx; display: flex; align-items: center;}
.required { color: #fa3534; margin-right: 8rpx; font-weight: bold; }
.img-row, .img-list { display: flex; flex-wrap: wrap; gap: 24rpx; }
.thumb { width: 160rpx; height: 160rpx; border-radius: 12rpx; box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.05); }
.upload-btn { width: 160rpx; height: 160rpx; background: #f4f5f7; border-radius: 12rpx; display: flex; flex-direction: column; align-items: center; justify-content: center; border: 2rpx dashed #dcdfe6; }
.upload-txt { font-size: 24rpx; color: #999; margin-top: 12rpx; }
.gallery-link {
display: inline-block;
margin-top: 12rpx;
font-size: 24rpx;
color: #6acdbb;
}
.contract-btn { width: 100%; height: 100rpx; flex-direction: row; gap: 12rpx; background: rgba(106, 205, 187, 0.05); border-color: #6acdbb; }
/* 底部操作区 */
.footer-actions { display: flex; flex-wrap: wrap; padding: 40rpx 24rpx 80rpx; gap: 24rpx; background: transparent; }
.btn-draft { flex: 1; min-width: 160rpx; }
.btn-prev { flex: 1; }
.btn-next, .btn-submit { flex: 2; }
.draft-name-box { padding: 40rpx 32rpx; background: #fff; }
.draft-name-title { display: block; font-size: 32rpx; font-weight: 600; margin-bottom: 24rpx; color: #1d2129; }
.draft-name-actions { display: flex; gap: 24rpx; margin-top: 32rpx; justify-content: flex-end; }
.success-panel {
display: flex;
flex-direction: column;
align-items: center;
padding: 120rpx 48rpx 80rpx;
min-height: 50vh;
}
.success-title { font-size: 36rpx; font-weight: 600; color: #1d2129; margin-top: 32rpx; }
.success-sub { font-size: 28rpx; color: #86909c; margin-top: 16rpx; text-align: center; }
.success-actions { display: flex; flex-direction: column; gap: 24rpx; width: 100%; margin-top: 64rpx; padding: 0 24rpx; box-sizing: border-box; }
</style>