1. 诊所管理

This commit is contained in:
李琦
2026-06-18 13:49:43 +08:00
parent 66a2243042
commit ef5067d015
13 changed files with 673 additions and 49 deletions

38
api/bankCardReport.js Normal file
View File

@@ -0,0 +1,38 @@
import { req } from '@/common/js/index.js';
import { unwrapClinicRes } from '@/api/clinicAdmin.js';
function getPrefix() {
const mode = uni.getStorageSync('loginMode');
if (mode === 'platform_admin') {
return { prefix: '/newApi/platform-admin-store-bank-card-report', headerKey: '_platformAdmin' };
}
return { prefix: '/newApi/salesperson-store-bank-card-report', headerKey: '_salesperson' };
}
function bizRequest(options) {
const { prefix, headerKey } = getPrefix();
return req.request({
...options,
url: `${prefix}${options.path}`,
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 };
});
}
export function getStoreBankCardReportDetail(storeId) {
return bizRequest({ path: '/detail', method: 'GET', data: { store_id: storeId } });
}
export function reportStoreBankCard(data) {
return bizRequest({ path: '/report', method: 'POST', data });
}
export function syncStoreBankCardStatusByStore(storeId) {
return bizRequest({ path: '/sync-status-by-store', method: 'POST', data: { store_id: storeId } });
}

View File

@@ -155,6 +155,9 @@ export function createBusinessApi(prefix, headerFlag) {
getPromoterOptions() {
return bizRequest({ url: `${prefix}-store-input/promoter-options`, method: 'GET' });
},
getPromoterDetail(params) {
return bizRequest({ url: `${prefix}-store-input/promoter-detail`, method: 'GET', data: params });
},
getStoreInputOption() {
return bizRequest({ url: `${prefix}-store-input/option`, method: 'GET' });
},

View File

@@ -28,6 +28,30 @@ function getOssSignatureByPrefix(prefix, headerKey) {
});
}
function registerOssFileByPrefix(prefix, headerKey, url) {
return req.request({
url: `${prefix}register-oss-file`,
method: 'POST',
data: { url, source: 2 },
header: {
[headerKey]: '1',
},
});
}
function registerBusinessOssFile(url) {
const mode = uni.getStorageSync('loginMode');
if (mode === 'platform_admin') {
return registerOssFileByPrefix(PLATFORM_PREFIX, '_platformAdmin', url);
}
if (mode === 'clinic_admin') {
return registerOssFileByPrefix(CLINIC_ADMIN_PREFIX, '_clinicAdmin', url);
}
return registerOssFileByPrefix(SALESPERSON_PREFIX, '_salesperson', url);
}
export { registerBusinessOssFile };
/**
* 业务员录入/聊天同款:服务端转存 OSS返回 url
*/

View File

@@ -4,7 +4,7 @@
* 运维:微信小程序 uploadFile 合法域名需包含 OSS host
* 例如 https://xiaokang88.oss-cn-hangzhou.aliyuncs.com
*/
import { getBusinessOssSignatureApi, unwrapOssSignature } from '@/api/upload.js';
import { getBusinessOssSignatureApi, unwrapOssSignature, registerBusinessOssFile } from '@/api/upload.js';
/**
* @param {string} filePath 本地临时文件路径
@@ -75,8 +75,12 @@ export async function uploadToOss(options) {
const code = uploadRes.statusCode;
if (code >= 200 && code < 300 || code === 204) {
const host = String(signature.host).replace(/\/$/, '');
const url = `${host}/${objectName}`;
registerBusinessOssFile(url).catch((err) => {
console.warn('OSS 文件登记失败:', err);
});
resolve({
url: `${host}/${objectName}`,
url,
objectName,
});
return;

View File

@@ -506,6 +506,7 @@
{ "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": "store-bank-card/report", "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" } }

View File

@@ -10,6 +10,9 @@
<text class="status" :class="'s' + item.status">{{ statusText(item.status) }}</text>
</view>
<view class="sub">{{ item.mobile }} · {{ formatTime(item.created_at) }}</view>
<view v-if="inputType === 'store' && item.promoter_admin" class="promoter-row" @click.stop>
<promoter-info-card :admin="item.promoter_admin" />
</view>
</view>
<view v-if="!list.length" class="empty">暂无记录</view>
</view>
@@ -18,11 +21,12 @@
<script>
import BusinessPageLayout from '@/subPackages/sub_business_shared/components/business-page-layout.vue'
import PromoterInfoCard from '@/subPackages/sub_salesperson/components/PromoterInfoCard.vue'
import businessMixin from '@/subPackages/sub_business_shared/common/businessMixin.js'
export default {
mixins: [businessMixin],
components: { BusinessPageLayout },
components: { BusinessPageLayout, PromoterInfoCard },
props: {
auditMode: { type: Boolean, default: false },
inputType: { type: String, default: 'store' },
@@ -82,5 +86,6 @@ export default {
.s1 { color: #00B42A; }
.s2 { color: #F53F3F; }
.sub { margin-top: 12rpx; color: #86909C; font-size: 24rpx; }
.promoter-row { margin-top: 16rpx; padding-top: 12rpx; border-top: 1rpx solid #f5f5f5; }
.empty { text-align: center; color: #86909C; padding: 80rpx 0; }
</style>

View File

@@ -57,11 +57,26 @@ function storeNamesText(info) {
}
function promoterText(info) {
const u = info.promoter || info.inputUser
const u = info.promoter_admin || info.promoter || info.inputUser
if (!u) return info.code || '-'
return [u.nick_name, u.code].filter(Boolean).join(' · ') || '-'
}
export function getPromoterAdmin(info) {
if (info?.promoter_admin) return info.promoter_admin
if (info?.code) {
return {
id: info.promoter?.id,
code: info.code,
nick_name: info.promoter?.nick_name,
avatar: info.promoter?.avatar,
phone: info.promoter?.phone,
role_name: info.promoter?.role_name,
}
}
return null
}
function bankTypeText(type) {
return Number(type) === 1 ? '对公' : '对私'
}
@@ -79,7 +94,7 @@ export function getStoreDetailSections() {
{ key: 'mobile', label: '联系电话' },
{ key: 'region', label: '省市区', format: (_, info) => regionText(info) },
{ key: 'position', label: '详细地址' },
{ key: 'code', label: '业务员', format: (_, info) => promoterText(info) },
{ key: 'code', label: '业务员', type: 'promoter' },
{ key: 'is_shipping_free', label: '是否包邮', format: (_, info) => yesNoText(info.is_shipping_free) },
{ key: 'subscribe_price_change', label: '订阅价格波动', format: (_, info) => yesNoText(info.subscribe_price_change) },
{

View File

@@ -0,0 +1,101 @@
<template>
<drawer-page-container v-model="visible" title="业务员详情">
<view v-if="loading" class="loading">加载中...</view>
<view v-else-if="detail" class="detail-wrap">
<view class="header">
<image v-if="detail.avatar" :src="detail.avatar" class="avatar" mode="aspectFill" />
<view v-else class="avatar placeholder">{{ (detail.nick_name || '?').charAt(0) }}</view>
<text class="name">{{ detail.nick_name || '-' }}</text>
<text class="role">{{ detail.role_name || '业务员' }}</text>
</view>
<view class="row"><text class="k">推广码</text><text class="v">{{ detail.code || '-' }}</text></view>
<view class="row"><text class="k">手机号</text><text class="v">{{ detail.phone || '-' }}</text></view>
<view class="row"><text class="k">录入诊所</text><text class="v">{{ detail.clinic_input_count || 0 }} </text></view>
<view class="row"><text class="k">录入药店</text><text class="v">{{ detail.pharmacy_input_count || 0 }} </text></view>
</view>
<view v-else class="loading">暂无数据</view>
</drawer-page-container>
</template>
<script>
import DrawerPageContainer from '@/subPackages/sub_salesperson/components/DrawerPageContainer.vue'
import { getInputBusinessContext } from '@/subPackages/sub_salesperson/common/context.js'
export default {
name: 'PromoterDetailPopup',
components: { DrawerPageContainer },
props: {
value: { type: Boolean, default: false },
admin: { type: Object, default: null },
},
data() {
return {
loading: false,
detail: null,
}
},
computed: {
visible: {
get() { return this.value },
set(v) { this.$emit('input', v) },
},
bizApi() {
return getInputBusinessContext().api
},
},
watch: {
value(open) {
if (open) this.loadDetail()
else this.detail = null
},
},
methods: {
loadDetail() {
const id = this.admin?.id
const code = this.admin?.code
if (!id && !code) return
this.loading = true
this.bizApi.getPromoterDetail({ id, code })
.then((w) => {
if (w.ok) this.detail = w.data || null
})
.finally(() => {
this.loading = false
})
},
},
}
</script>
<style scoped>
.loading { text-align: center; color: #86909c; padding: 80rpx 0; font-size: 28rpx; }
.detail-wrap { padding: 0 32rpx 32rpx; }
.header {
display: flex;
flex-direction: column;
align-items: center;
gap: 12rpx;
padding: 32rpx 0 40rpx;
}
.avatar { width: 128rpx; height: 128rpx; border-radius: 50%; }
.avatar.placeholder {
background: #e8f7f4;
color: #6acdbb;
display: flex;
align-items: center;
justify-content: center;
font-size: 48rpx;
font-weight: 600;
}
.name { font-size: 34rpx; font-weight: 600; color: #1d2129; }
.role { font-size: 26rpx; color: #86909c; }
.row {
display: flex;
justify-content: space-between;
padding: 24rpx 0;
border-bottom: 1rpx solid #f0f0f0;
font-size: 28rpx;
}
.k { color: #86909c; }
.v { color: #1d2129; max-width: 420rpx; text-align: right; word-break: break-all; }
</style>

View File

@@ -0,0 +1,80 @@
<template>
<view class="promoter-info-card">
<view
v-if="displayAdmin"
class="card-body"
:class="{ clickable: clickable && canOpenDetail }"
@click.stop="openDetail"
>
<image v-if="displayAdmin.avatar" :src="displayAdmin.avatar" class="avatar" mode="aspectFill" />
<view v-else class="avatar placeholder">{{ displayName.charAt(0) }}</view>
<view class="meta">
<text class="name">{{ displayName }}</text>
<text v-if="displayAdmin.phone" class="sub">{{ displayAdmin.phone }}</text>
<text v-else-if="displayAdmin.code" class="sub">业务码{{ displayAdmin.code }}</text>
</view>
</view>
<text v-else class="empty">-</text>
<promoter-detail-popup v-model="showDetail" :admin="displayAdmin" />
</view>
</template>
<script>
import PromoterDetailPopup from '@/subPackages/sub_salesperson/components/PromoterDetailPopup.vue'
export default {
name: 'PromoterInfoCard',
components: { PromoterDetailPopup },
props: {
admin: { type: Object, default: null },
clickable: { type: Boolean, default: true },
},
data() {
return { showDetail: false }
},
computed: {
displayAdmin() {
if (!this.admin) return null
if (this.admin.id || this.admin.code || this.admin.nick_name) return this.admin
return null
},
displayName() {
return this.displayAdmin?.nick_name?.trim() || this.displayAdmin?.phone?.trim() || '-'
},
canOpenDetail() {
return !!(this.displayAdmin?.id || this.displayAdmin?.code)
},
},
methods: {
openDetail() {
if (!this.clickable || !this.canOpenDetail) return
this.showDetail = true
},
},
}
</script>
<style scoped>
.promoter-info-card { min-width: 0; }
.card-body {
display: flex;
align-items: center;
gap: 16rpx;
min-width: 0;
}
.card-body.clickable:active { opacity: 0.85; }
.avatar { width: 56rpx; height: 56rpx; border-radius: 50%; flex-shrink: 0; }
.avatar.placeholder {
background: #e8f7f4;
color: #6acdbb;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
font-weight: 600;
}
.meta { flex: 1; min-width: 0; }
.name { display: block; font-size: 26rpx; color: #1d2129; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sub { display: block; font-size: 22rpx; color: #86909c; margin-top: 4rpx; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.empty { font-size: 26rpx; color: #86909c; }
</style>

View File

@@ -1,44 +1,65 @@
<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 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">{{ item.nick_name || '-' }}</text>
<text class="sub">{{ item.code }} · {{ item.role_name }}</text>
<text class="name">{{ selected.nick_name || '-' }}</text>
<text class="sub">{{ selectedSubText }}</text>
</view>
<u-icon v-if="value === item.code" name="checkmark" color="#6acdbb" size="32"></u-icon>
</view>
</scroll-view>
</drawer-page-container>
<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="选择业务员">
<view class="search-wrap">
<u-input
v-model="searchKeyword"
placeholder="搜索姓名、业务码、手机号"
:clearable="true"
border="surround"
/>
</view>
<scroll-view scroll-y class="sheet-scroll">
<view v-if="loading" class="empty">加载中...</view>
<view v-else-if="!filteredOptions.length" class="empty">无匹配业务员</view>
<view
v-for="item in filteredOptions"
: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>
<text v-if="item.phone" class="phone">{{ item.phone }}</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'
function filterPromoterOptions(keyword, list) {
const q = String(keyword || '').trim().toLowerCase()
if (!q) return list
return list.filter((item) => {
const name = (item.nick_name || '').toLowerCase()
const code = (item.code || '').toLowerCase()
const phone = (item.phone || '').toLowerCase()
const role = (item.role_name || '').toLowerCase()
return name.includes(q) || code.includes(q) || phone.includes(q) || role.includes(q)
})
}
export default {
name: 'PromoterPicker',
components: { DrawerPageContainer },
@@ -50,16 +71,31 @@ export default {
loading: { type: Boolean, default: false },
},
data() {
return { showPopup: false }
return {
showPopup: false,
searchKeyword: '',
}
},
computed: {
selected() {
return this.options.find((o) => o.code === this.value) || null
},
filteredOptions() {
return filterPromoterOptions(this.searchKeyword, this.options)
},
selectedSubText() {
if (!this.selected) return ''
const parts = []
if (this.selected.code) parts.push(this.selected.code)
if (this.selected.phone) parts.push(this.selected.phone)
else if (this.selected.role_name) parts.push(this.selected.role_name)
return parts.join(' · ')
},
},
methods: {
onOpen() {
if (this.disabled) return
this.searchKeyword = ''
this.showPopup = true
},
onPick(item) {
@@ -72,9 +108,7 @@ export default {
</script>
<style scoped>
.picker-wrap {
width: 100%;
}
.picker-wrap { width: 100%; }
.promoter-picker {
display: flex;
align-items: center;
@@ -98,8 +132,10 @@ export default {
.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; }
.phone { display: block; font-size: 22rpx; color: #86909c; margin-top: 4rpx; }
.placeholder-text { font-size: 28rpx; color: #c0c4cc; flex: 1; }
.sheet-scroll { max-height: 60vh; padding-bottom: 24rpx; }
.search-wrap { padding: 0 32rpx 16rpx; }
.sheet-scroll { max-height: 55vh; padding-bottom: 24rpx; }
.option-item {
display: flex;
align-items: center;

View File

@@ -44,6 +44,9 @@
@click="openFile(file.url)"
>{{ file.name }}</text>
</view>
<view v-else-if="field.type === 'promoter'" class="v-promoter">
<promoter-info-card :admin="promoterAdmin" />
</view>
<text v-else class="v-text">{{ fieldValue(field) }}</text>
</view>
</view>
@@ -80,6 +83,9 @@
<view class="footer-actions">
<u-button v-if="canEdit" class="action-btn" type="primary" :custom-style="primaryStyle" @click="goEdit">继续编辑</u-button>
<u-button v-if="showDoctorEntry" class="action-btn" type="primary" :custom-style="primaryStyle" @click="goDoctorInput">录入医生</u-button>
<u-button v-if="showBankCardReport" class="action-btn" type="primary" plain @click="goBankCardReport">
{{ bankCardReportButtonText }}
</u-button>
<template v-if="canAudit">
<u-button class="action-btn reject" @click="audit(2)">审核拒绝</u-button>
<u-button class="action-btn" type="primary" :custom-style="primaryStyle" @click="audit(1)">审核通过</u-button>
@@ -91,6 +97,7 @@
<script>
import BusinessPageLayout from '@/subPackages/sub_salesperson/components/business-page-layout.vue'
import PromoterInfoCard from '@/subPackages/sub_salesperson/components/PromoterInfoCard.vue'
import businessMixin from '@/subPackages/sub_salesperson/common/businessMixin.js'
import {
getStoreDetailSections,
@@ -99,13 +106,14 @@ import {
getDetailImageUrls,
getDetailFileLinks,
getLinkedDoctorInputs,
getPromoterAdmin,
inputStatusText,
} from '@/subPackages/sub_salesperson/common/inputDetailFields.js'
import { goDoctorInputFromStore } from '@/subPackages/sub_salesperson/common/inputNavigation.js'
export default {
mixins: [businessMixin],
components: { BusinessPageLayout },
components: { BusinessPageLayout, PromoterInfoCard },
props: {
recordId: { type: Number, default: 0 },
inputType: { type: String, default: 'store' },
@@ -150,6 +158,19 @@ export default {
linkedDoctors() {
return getLinkedDoctorInputs(this.info)
},
promoterAdmin() {
return getPromoterAdmin(this.info)
},
showBankCardReport() {
return this.inputType === 'store'
&& Number(this.info.status) === 1
&& Number(this.info.synced_store_id) > 0
},
bankCardReportButtonText() {
const text = this.info.bank_card_report_status_text
if (text && text !== '未报备') return `银行卡报备(${text}`
return '银行卡报备'
},
},
watch: {
recordId: {
@@ -192,6 +213,9 @@ export default {
},
shouldShowField(field) {
if (field.show && !field.show(this.info)) return false
if (field.type === 'promoter') {
return !!getPromoterAdmin(this.info)
}
if (field.type === 'image' || field.type === 'images') {
return getDetailImageUrls(field, this.info).length > 0
}
@@ -226,6 +250,13 @@ export default {
goDoctorInput() {
goDoctorInputFromStore(this.info.id, this.bizCtx)
},
goBankCardReport() {
const storeId = Number(this.info.synced_store_id || 0)
if (!storeId) return
uni.navigateTo({
url: `/subPackages/sub_salesperson/store-bank-card/report?store_id=${storeId}&store_input_id=${this.info.id}`,
})
},
goDoctorDetail(doctorId) {
if (!doctorId || !this.doctorDetailPath) return
uni.navigateTo({ url: `${this.doctorDetailPath}?id=${doctorId}` })

View File

@@ -0,0 +1,278 @@
<template>
<business-page-layout title="银行卡报备" :show-back="true">
<view class="page-wrap">
<view v-if="loading" class="loading">加载中...</view>
<view v-else>
<view v-if="reportInfo && reportInfo.id" class="status-card">
<text class="status-label">报备状态</text>
<text class="status-value">{{ reportStatusText }}</text>
<u-button size="mini" class="sync-btn" @click="syncStatus">刷新状态</u-button>
</view>
<view v-if="!hasReport || canSubmitNew" class="form-wrap">
<view class="tip">新报备需上传合同附件字段与 PC 端一致</view>
<u-form :model="form" label-width="180">
<u-form-item label="开户人姓名" :required="true">
<u-input v-model="form.bank_user_name" placeholder="请输入开户人姓名" />
</u-form-item>
<u-form-item label="银行卡号" :required="true">
<u-input v-model="form.bank_card" type="number" placeholder="请输入银行卡号" />
</u-form-item>
<u-form-item label="开户银行" :required="true">
<u-input v-model="form.bank_name" placeholder="请输入开户银行名称" />
</u-form-item>
<u-form-item label="账户类型" :required="true">
<u-input :value="accountTypeLabel" type="select" placeholder="请选择" @click="showAccountType = true" />
<u-select v-model="showAccountType" :list="accountTypeOptions" @confirm="onAccountTypeConfirm" />
</u-form-item>
<u-form-item label="银联行号">
<u-input v-model="form.bank_no" placeholder="对公/非62开头卡号必填" />
</u-form-item>
<u-form-item label="证件号码">
<u-input v-model="form.cert_no" placeholder="选填" />
</u-form-item>
<u-form-item label="合同附件" :required="true">
<view class="attachment-row">
<text v-if="form.attachment" class="attachment-name" @click="previewAttachment">{{ attachmentName }}</text>
<text v-else class="attachment-placeholder">请上传合同附件</text>
<u-button size="mini" @click="uploadAttachment">上传</u-button>
</view>
</u-form-item>
<u-form-item label="备注">
<u-input v-model="form.remark" placeholder="选填" />
</u-form-item>
<u-form-item label="摘要">
<u-input v-model="form.summary" placeholder="选填" />
</u-form-item>
</u-form>
<u-button type="primary" :custom-style="primaryStyle" :loading="submitting" @click="submit">提交报备</u-button>
</view>
</view>
</view>
</business-page-layout>
</template>
<script>
import BusinessPageLayout from '@/subPackages/sub_salesperson/components/business-page-layout.vue'
import { getStoreBankCardReportDetail, reportStoreBankCard, syncStoreBankCardStatusByStore } from '@/api/bankCardReport.js'
import { pickAndUploadImage } from '@/subPackages/sub_salesperson/common/inputFormHelpers.js'
import { uploadToOss } from '@/common/js/oss-upload.js'
import { prepareImagePath } from '@/common/js/image-compress.js'
const ACCOUNT_TYPES = [
{ label: '对公账户', value: '1' },
{ label: '法人账户', value: '2' },
{ label: '授权对公账户', value: '3' },
{ label: '授权个人账户', value: '4' },
]
export default {
components: { BusinessPageLayout },
data() {
return {
storeId: 0,
storeInputId: 0,
loading: true,
submitting: false,
reportInfo: null,
showAccountType: false,
form: {
bank_user_name: '',
bank_card: '',
bank_name: '',
account_type: '2',
bank_no: '',
cert_no: '',
attachment: '',
remark: '',
summary: '',
use_type: '2',
},
primaryStyle: {
backgroundColor: '#6acdbb',
color: '#ffffff',
borderColor: '#6acdbb',
marginTop: '32rpx',
},
}
},
computed: {
accountTypeOptions() {
return ACCOUNT_TYPES
},
accountTypeLabel() {
const item = ACCOUNT_TYPES.find((o) => o.value === this.form.account_type)
return item ? item.label : '请选择'
},
hasReport() {
return !!(this.reportInfo && this.reportInfo.id)
},
canSubmitNew() {
if (!this.hasReport) return true
return [3, 4].includes(Number(this.reportInfo.report_status))
},
reportStatusText() {
const s = Number(this.reportInfo?.report_status)
if (s === 2) return '有效'
if (s === 3) return '审核不通过'
if (s === 4) return '无效'
if (s === 1) return '待审核'
return this.reportInfo?.report_status_text || '未知'
},
attachmentName() {
if (!this.form.attachment) return ''
return this.form.attachment.split('/').pop() || '已上传'
},
},
onLoad(q) {
this.storeId = Number(q.store_id || 0)
this.storeInputId = Number(q.store_input_id || 0)
this.loadDetail()
},
methods: {
loadDetail() {
if (!this.storeId) {
this.loading = false
return
}
this.loading = true
getStoreBankCardReportDetail(this.storeId)
.then((w) => {
if (w.ok && w.data) {
this.reportInfo = w.data
this.applyDraft(w.data)
}
})
.finally(() => {
this.loading = false
})
},
applyDraft(data) {
if (!data || data.id) return
this.form.bank_user_name = data.bank_user_name || ''
this.form.bank_card = data.bank_card || ''
this.form.bank_name = data.bank_name || ''
this.form.account_type = String(data.account_type || data.bank_account_type || '2')
this.form.bank_no = data.bank_no || ''
this.form.cert_no = data.cert_no || ''
},
onAccountTypeConfirm(e) {
const item = e[0]
if (item) this.form.account_type = String(item.value)
},
uploadAttachment() {
uni.showActionSheet({
itemList: ['相册/拍照', '选择文件'],
success: (res) => {
if (res.tapIndex === 0) {
pickAndUploadImage().then((url) => {
this.form.attachment = url
}).catch(() => {})
} else {
this.chooseAndUploadFile()
}
},
})
},
chooseAndUploadFile() {
uni.chooseMessageFile({
count: 1,
type: 'file',
success: async (res) => {
const file = res.tempFiles && res.tempFiles[0]
if (!file) return
uni.showLoading({ title: '上传中...', mask: true })
try {
const prepared = await prepareImagePath({ path: file.path, size: file.size })
const uploadPath = prepared ? prepared.path : file.path
const result = await uploadToOss({ filePath: uploadPath })
this.form.attachment = result.url
} catch (e) {
uni.showToast({ title: e.message || '上传失败', icon: 'none' })
} finally {
uni.hideLoading()
}
},
})
},
previewAttachment() {
if (!this.form.attachment) return
const url = this.form.attachment
if (/\.(jpg|jpeg|png|gif|webp)$/i.test(url)) {
uni.previewImage({ urls: [url] })
} else {
uni.setClipboardData({ data: url })
uni.showToast({ title: '链接已复制', icon: 'none' })
}
},
validateForm() {
const f = this.form
if (!f.bank_user_name?.trim()) return this.toast('请输入开户人姓名')
if (!f.bank_card?.trim()) return this.toast('请输入银行卡号')
if (!f.bank_name?.trim()) return this.toast('请输入开户银行')
if (!f.account_type) return this.toast('请选择账户类型')
if (!f.attachment?.trim()) return this.toast('请上传合同附件')
return true
},
toast(msg) {
uni.showToast({ title: msg, icon: 'none' })
return false
},
submit() {
if (!this.validateForm()) return
this.submitting = true
reportStoreBankCard({
store_id: this.storeId,
store_input_id: this.storeInputId,
...this.form,
bank_account_type: this.mapAccountTypeToStore(this.form.account_type),
})
.then((w) => {
if (w.ok) {
uni.showToast({ title: '报备已提交', icon: 'success' })
setTimeout(() => this.loadDetail(), 500)
} else {
uni.showToast({ title: (w.res && w.res.msg) || '提交失败', icon: 'none' })
}
})
.finally(() => {
this.submitting = false
})
},
mapAccountTypeToStore(accountType) {
const map = { '1': 1, '2': 2, '3': 1, '4': 2 }
return map[String(accountType)] || 2
},
syncStatus() {
syncStoreBankCardStatusByStore(this.storeId).then((w) => {
if (w.ok) {
this.reportInfo = w.data
uni.showToast({ title: '已刷新', icon: 'success' })
}
})
},
},
}
</script>
<style scoped>
.page-wrap { padding: 24rpx; }
.loading { text-align: center; color: #86909c; padding: 80rpx 0; }
.status-card {
background: #fff;
border-radius: 16rpx;
padding: 28rpx;
margin-bottom: 24rpx;
display: flex;
align-items: center;
gap: 16rpx;
flex-wrap: wrap;
}
.status-label { color: #86909c; font-size: 26rpx; }
.status-value { color: #1d2129; font-size: 30rpx; font-weight: 600; flex: 1; }
.form-wrap { background: #fff; border-radius: 16rpx; padding: 24rpx; }
.tip { font-size: 24rpx; color: #86909c; margin-bottom: 24rpx; line-height: 1.5; }
.attachment-row { display: flex; align-items: center; gap: 16rpx; width: 100%; }
.attachment-name { flex: 1; font-size: 24rpx; color: #165dff; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.attachment-placeholder { flex: 1; font-size: 26rpx; color: #c0c4cc; }
</style>

View File

@@ -132,20 +132,21 @@
</u-form-item>
</view>
<!-- ================= 步骤 3结算信息 ================= -->
<!-- ================= 步骤 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="开户人姓名" :required="true">
<u-form-item label="开户人姓名">
<u-input v-model="form.bank_user_name" placeholder="请输入开户人姓名" />
</u-form-item>
<u-form-item label="银行卡号" :required="true">
<u-form-item label="银行卡号">
<u-input v-model="form.bank_card" type="number" placeholder="请输入银行卡号" />
</u-form-item>
<u-form-item label="开户行" :required="true">
<u-form-item label="开户行">
<u-input v-model="form.bank_name" placeholder="请输入开户行名称 (如: 招商银行)" />
</u-form-item>
<u-form-item label="银联行号">
@@ -438,13 +439,19 @@ export default {
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() {
// 最终提交前校验当前(最后一步)的信息
const f = this.form;
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('请输入开户行');
if (!this.validateOptionalBankFields()) return
const payload = {
...this.form,
@@ -501,6 +508,7 @@ export default {
/* 标题 */
.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; }
/* 营业时间特别处理 */