1. 修复了一些问题
This commit is contained in:
235
subPackages/sub_salesperson_manage/commission/index.vue
Normal file
235
subPackages/sub_salesperson_manage/commission/index.vue
Normal file
@@ -0,0 +1,235 @@
|
||||
<template>
|
||||
<page-layout :is-platform="ctx.isPlatform" :title="pageTitle" :show-back="true">
|
||||
<view class="page">
|
||||
<view class="tabs">
|
||||
<view
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="tab"
|
||||
:class="{ active: activeTab === tab.key }"
|
||||
@click="switchTab(tab.key)"
|
||||
>{{ tab.label }}</view>
|
||||
</view>
|
||||
|
||||
<view v-if="activeTab !== 'history'" class="toolbar">
|
||||
<input class="search" v-model="orderNo" placeholder="订单号" @confirm="loadOrders" />
|
||||
<view class="search-btn" @click="loadOrders">查询</view>
|
||||
</view>
|
||||
|
||||
<template v-if="activeTab === 'pending'">
|
||||
<view class="settle-actions">
|
||||
<view class="settle-btn" @click="goPeriodSettle">按时间段结算</view>
|
||||
<view class="settle-btn primary" @click="goOrderSettle">结算选中订单</view>
|
||||
</view>
|
||||
<view v-for="item in orderList" :key="item.order_id" class="order-row" @click="toggleSelect(item)">
|
||||
<view class="checkbox" :class="{ checked: selectedIds.includes(item.order_id) }" />
|
||||
<view class="order-info">
|
||||
<text class="order-no">{{ item.order_no }}</text>
|
||||
<text class="sub">明细 {{ item.item_count }} 条 · {{ item.order_pay_at_text }}</text>
|
||||
</view>
|
||||
<text class="amount">¥{{ item.commission_amount }}</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<template v-else-if="activeTab === 'settled'">
|
||||
<view v-for="item in orderList" :key="item.order_id" class="order-row" @click="showOrderDetail(item)">
|
||||
<view class="order-info">
|
||||
<text class="order-no">{{ item.order_no }}</text>
|
||||
<text class="sub">明细 {{ item.item_count }} 条 · {{ item.order_pay_at_text }}</text>
|
||||
</view>
|
||||
<text class="amount">¥{{ item.commission_amount }}</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<view v-for="item in settlementList" :key="item.id" class="order-row" @click="goSettlementDetail(item.id)">
|
||||
<view class="order-info">
|
||||
<text class="order-no">{{ item.settlement_no }}</text>
|
||||
<text class="sub">{{ item.settlement_type_text }} · {{ item.created_at_text }}</text>
|
||||
</view>
|
||||
<text class="amount">¥{{ item.amount }}</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<view v-if="!loading && isEmpty" class="empty">暂无数据</view>
|
||||
</view>
|
||||
</page-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PageLayout from '../components/PageLayout.vue';
|
||||
import { getSalespersonManageContext, resolveManageMode } from '../common/context.js';
|
||||
|
||||
const ROOT = '/subPackages/sub_salesperson_manage';
|
||||
|
||||
export default {
|
||||
components: { PageLayout },
|
||||
data() {
|
||||
return {
|
||||
ctx: getSalespersonManageContext('clinic'),
|
||||
salespersonId: 0,
|
||||
storeId: 0,
|
||||
nickName: '',
|
||||
activeTab: 'pending',
|
||||
tabs: [
|
||||
{ key: 'pending', label: '待结算' },
|
||||
{ key: 'settled', label: '已结算' },
|
||||
{ key: 'history', label: '结算历史' },
|
||||
],
|
||||
orderNo: '',
|
||||
orderList: [],
|
||||
settlementList: [],
|
||||
selectedIds: [],
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
pageTitle() {
|
||||
return this.nickName ? `分成与结算 - ${this.nickName}` : '分成与结算';
|
||||
},
|
||||
isEmpty() {
|
||||
if (this.activeTab === 'history') return !this.settlementList.length;
|
||||
return !this.orderList.length;
|
||||
},
|
||||
},
|
||||
onLoad(options) {
|
||||
this.ctx = getSalespersonManageContext(resolveManageMode(options));
|
||||
this.salespersonId = Number(options.salesperson_id || 0);
|
||||
this.storeId = Number(options.store_id || 0);
|
||||
this.nickName = decodeURIComponent(options.nick_name || '');
|
||||
this.loadData();
|
||||
},
|
||||
methods: {
|
||||
switchTab(key) {
|
||||
this.activeTab = key;
|
||||
this.selectedIds = [];
|
||||
this.loadData();
|
||||
},
|
||||
loadData() {
|
||||
if (this.activeTab === 'history') this.loadSettlements();
|
||||
else this.loadOrders();
|
||||
},
|
||||
buildParams() {
|
||||
const params = {
|
||||
salesperson_id: this.salespersonId,
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
order_no: this.orderNo,
|
||||
};
|
||||
if (this.storeId > 0) params.store_id = this.storeId;
|
||||
if (this.activeTab === 'pending') params.status = 0;
|
||||
if (this.activeTab === 'settled') params.status = 1;
|
||||
return params;
|
||||
},
|
||||
async loadOrders() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const wrap = await this.ctx.api.getCommissionOrderList(this.buildParams());
|
||||
this.orderList = (wrap && wrap.ok && wrap.data && wrap.data.items) ? wrap.data.items : [];
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
async loadSettlements() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const params = { salesperson_id: this.salespersonId, page: 1, pageSize: 50 };
|
||||
if (this.storeId > 0) params.store_id = this.storeId;
|
||||
const wrap = await this.ctx.api.getSettlementList(params);
|
||||
this.settlementList = (wrap && wrap.ok && wrap.data && wrap.data.items) ? wrap.data.items : [];
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
toggleSelect(item) {
|
||||
const id = item.order_id;
|
||||
const idx = this.selectedIds.indexOf(id);
|
||||
if (idx >= 0) this.selectedIds.splice(idx, 1);
|
||||
else this.selectedIds.push(id);
|
||||
},
|
||||
goPeriodSettle() {
|
||||
uni.navigateTo({
|
||||
url: `${ROOT}/settlement/preview?mode=${this.ctx.mode}&salesperson_id=${this.salespersonId}&store_id=${this.storeId}`,
|
||||
});
|
||||
},
|
||||
async goOrderSettle() {
|
||||
if (!this.selectedIds.length) {
|
||||
uni.showToast({ title: '请勾选订单', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
const params = {
|
||||
salesperson_id: this.salespersonId,
|
||||
settlement_type: 2,
|
||||
order_ids: this.selectedIds.join(','),
|
||||
};
|
||||
if (this.storeId > 0) params.store_id = this.storeId;
|
||||
uni.showLoading({ title: '预览中' });
|
||||
try {
|
||||
const wrap = await this.ctx.api.getSettlementPreview(params);
|
||||
if (!wrap || !wrap.ok) return;
|
||||
const preview = wrap.data || {};
|
||||
const q = [
|
||||
`mode=${this.ctx.mode}`,
|
||||
`salesperson_id=${this.salespersonId}`,
|
||||
`store_id=${this.storeId}`,
|
||||
'settlement_type=2',
|
||||
`order_ids=${this.selectedIds.join(',')}`,
|
||||
`amount=${encodeURIComponent(preview.amount || '0.00')}`,
|
||||
`record_count=${preview.record_count || 0}`,
|
||||
].join('&');
|
||||
uni.navigateTo({ url: `${ROOT}/settlement/confirm?${q}` });
|
||||
} finally {
|
||||
uni.hideLoading();
|
||||
}
|
||||
},
|
||||
async showOrderDetail(item) {
|
||||
const params = {
|
||||
salesperson_id: this.salespersonId,
|
||||
order_id: item.order_id,
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
};
|
||||
if (this.storeId > 0) params.store_id = this.storeId;
|
||||
const wrap = await this.ctx.api.getCommissionList(params);
|
||||
const items = (wrap && wrap.ok && wrap.data && wrap.data.items) ? wrap.data.items : [];
|
||||
const lines = items.map((r) => `${r.drug_info || ''} ¥${r.commission_amount}`).join('\n');
|
||||
uni.showModal({
|
||||
title: `订单 ${item.order_no}`,
|
||||
content: lines || '无明细',
|
||||
showCancel: false,
|
||||
});
|
||||
},
|
||||
goSettlementDetail(id) {
|
||||
uni.navigateTo({
|
||||
url: `${ROOT}/settlement/detail?mode=${this.ctx.mode}&id=${id}&store_id=${this.storeId}`,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page { padding: 24rpx; }
|
||||
.tabs { display: flex; background: #fff; border-radius: 16rpx; margin-bottom: 20rpx; overflow: hidden; }
|
||||
.tab { flex: 1; text-align: center; padding: 20rpx; font-size: 26rpx; color: #666; }
|
||||
.tab.active { background: #6ACDBB; color: #fff; font-weight: 600; }
|
||||
.toolbar { display: flex; gap: 12rpx; margin-bottom: 16rpx; }
|
||||
.search { flex: 1; background: #fff; border-radius: 12rpx; padding: 16rpx; font-size: 26rpx; }
|
||||
.search-btn { background: #6ACDBB; color: #fff; padding: 16rpx 24rpx; border-radius: 12rpx; font-size: 26rpx; }
|
||||
.settle-actions { display: flex; gap: 12rpx; margin-bottom: 16rpx; }
|
||||
.settle-btn { flex: 1; text-align: center; padding: 16rpx; background: #fff; border-radius: 12rpx; font-size: 26rpx; color: #6ACDBB; border: 1rpx solid #6ACDBB; }
|
||||
.settle-btn.primary { background: #6ACDBB; color: #fff; border: none; }
|
||||
.order-row {
|
||||
display: flex; align-items: center; background: #fff; border-radius: 16rpx;
|
||||
padding: 24rpx; margin-bottom: 12rpx;
|
||||
}
|
||||
.checkbox {
|
||||
width: 36rpx; height: 36rpx; border: 2rpx solid #ddd; border-radius: 8rpx; margin-right: 16rpx;
|
||||
}
|
||||
.checkbox.checked { background: #6ACDBB; border-color: #6ACDBB; }
|
||||
.order-info { flex: 1; }
|
||||
.order-no { display: block; font-size: 28rpx; font-weight: 600; }
|
||||
.sub { font-size: 22rpx; color: #999; }
|
||||
.amount { font-size: 30rpx; color: #F97316; font-weight: 600; }
|
||||
.empty { text-align: center; color: #999; padding: 80rpx 0; }
|
||||
</style>
|
||||
23
subPackages/sub_salesperson_manage/common/context.js
Normal file
23
subPackages/sub_salesperson_manage/common/context.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import { createSalespersonManageApi } from '@/api/salespersonManage.js';
|
||||
|
||||
const ROOT = '/subPackages/sub_salesperson_manage';
|
||||
|
||||
export function resolveManageMode(options) {
|
||||
const mode = (options && options.mode) || '';
|
||||
if (mode === 'platform' || mode === 'clinic') return mode;
|
||||
const loginMode = uni.getStorageSync('loginMode');
|
||||
return loginMode === 'platform_admin' ? 'platform' : 'clinic';
|
||||
}
|
||||
|
||||
export function getSalespersonManageContext(mode) {
|
||||
const api = createSalespersonManageApi(mode);
|
||||
return {
|
||||
mode,
|
||||
api,
|
||||
isPlatform: mode === 'platform',
|
||||
listPath: `${ROOT}/index?mode=${mode}`,
|
||||
homePath: mode === 'platform'
|
||||
? '/subPackages/sub_platform_admin/home/index'
|
||||
: '/subPackages/sub_clinic_admin/home/index',
|
||||
};
|
||||
}
|
||||
17
subPackages/sub_salesperson_manage/common/labels.js
Normal file
17
subPackages/sub_salesperson_manage/common/labels.js
Normal file
@@ -0,0 +1,17 @@
|
||||
export function splitTypeLabel(type) {
|
||||
return Number(type) === 1 ? '百分比' : '固定金额';
|
||||
}
|
||||
|
||||
export function tcmBaseLabel(type) {
|
||||
return Number(type) === 1 ? '处方总价' : '药店利润';
|
||||
}
|
||||
|
||||
export const SPLIT_TYPE_OPTIONS = [
|
||||
{ label: '元/件(固定金额)', value: 0 },
|
||||
{ label: '百分比(%)', value: 1 },
|
||||
];
|
||||
|
||||
export const TCM_BASE_OPTIONS = [
|
||||
{ label: '药店利润', value: 0 },
|
||||
{ label: '处方总价', value: 1 },
|
||||
];
|
||||
81
subPackages/sub_salesperson_manage/components/PageLayout.vue
Normal file
81
subPackages/sub_salesperson_manage/components/PageLayout.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<view class="manage-page-layout safe-area-inset-bottom">
|
||||
<u-navbar
|
||||
:title="title"
|
||||
:is-back="showBack"
|
||||
:is-fixed="true"
|
||||
:border-bottom="false"
|
||||
title-size="34"
|
||||
title-color="#fff"
|
||||
back-icon-color="#fff"
|
||||
:background="{ background: 'linear-gradient(90deg, #00BFA6 0%, #6ACDBB 93%)' }"
|
||||
/>
|
||||
<view class="page-body">
|
||||
<slot />
|
||||
</view>
|
||||
<u-tabbar
|
||||
v-if="showTabbar"
|
||||
:value="tabIndex"
|
||||
:list="tabList"
|
||||
active-color="#6ACDBB"
|
||||
@change="onTabChange"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getPlatformAdminContext } from '@/subPackages/sub_platform_admin/common/context.js';
|
||||
import { getClinicTabList, switchClinicTab } from '@/subPackages/sub_clinic_admin/common/tabbar.js';
|
||||
|
||||
export default {
|
||||
name: 'SalespersonManagePageLayout',
|
||||
props: {
|
||||
isPlatform: { type: Boolean, default: false },
|
||||
title: { type: String, default: '' },
|
||||
showBack: { type: Boolean, default: true },
|
||||
showTabbar: { type: Boolean, default: false },
|
||||
tabIndex: { type: Number, default: 0 },
|
||||
},
|
||||
provide() {
|
||||
if (this.isPlatform) {
|
||||
return { businessContext: getPlatformAdminContext() };
|
||||
}
|
||||
return {};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tabList: [],
|
||||
};
|
||||
},
|
||||
created() {
|
||||
if (this.showTabbar) {
|
||||
this.tabList = this.isPlatform
|
||||
? (getPlatformAdminContext().tabList || [])
|
||||
: getClinicTabList();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onTabChange(index) {
|
||||
if (index === this.tabIndex) return;
|
||||
const target = this.tabList[index];
|
||||
if (!target || !target.pagePath) return;
|
||||
if (this.isPlatform) {
|
||||
uni.reLaunch({ url: target.pagePath });
|
||||
} else {
|
||||
switchClinicTab(index);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.manage-page-layout {
|
||||
min-height: 100vh;
|
||||
background: #f9fafb;
|
||||
}
|
||||
.page-body {
|
||||
padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<view class="proof-uploader">
|
||||
<view class="label">结算凭证(至少1张)</view>
|
||||
<view class="image-list">
|
||||
<view v-for="(url, idx) in images" :key="url" class="image-item">
|
||||
<image class="thumb" :src="url" mode="aspectFill" @click="preview(idx)" />
|
||||
<view class="remove" @click="remove(idx)">×</view>
|
||||
</view>
|
||||
<view v-if="images.length < maxCount" class="add-btn" @click="chooseImage">
|
||||
<u-icon name="plus" size="40" color="#999" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { uploadSalespersonManageFileApi, extractUploadUrl } from '@/api/upload.js';
|
||||
|
||||
export default {
|
||||
name: 'ProofImageUploader',
|
||||
props: {
|
||||
value: { type: Array, default: () => [] },
|
||||
maxCount: { type: Number, default: 5 },
|
||||
},
|
||||
computed: {
|
||||
images: {
|
||||
get() { return this.value || []; },
|
||||
set(v) { this.$emit('input', v); },
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
chooseImage() {
|
||||
const remain = this.maxCount - this.images.length;
|
||||
uni.chooseImage({
|
||||
count: remain,
|
||||
sizeType: ['compressed'],
|
||||
success: async (res) => {
|
||||
const paths = res.tempFilePaths || [];
|
||||
for (const filePath of paths) {
|
||||
try {
|
||||
uni.showLoading({ title: '上传中' });
|
||||
const uploadRes = await uploadSalespersonManageFileApi({ filePath });
|
||||
const url = extractUploadUrl(uploadRes);
|
||||
if (url) {
|
||||
this.$emit('input', [...this.images, url]);
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '上传失败', icon: 'none' });
|
||||
} finally {
|
||||
uni.hideLoading();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
remove(idx) {
|
||||
const next = [...this.images];
|
||||
next.splice(idx, 1);
|
||||
this.$emit('input', next);
|
||||
},
|
||||
preview(idx) {
|
||||
uni.previewImage({ urls: this.images, current: this.images[idx] });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.proof-uploader { margin-top: 24rpx; }
|
||||
.label { font-size: 26rpx; color: #666; margin-bottom: 16rpx; }
|
||||
.image-list { display: flex; flex-wrap: wrap; gap: 16rpx; }
|
||||
.image-item { position: relative; width: 160rpx; height: 160rpx; }
|
||||
.thumb { width: 100%; height: 100%; border-radius: 12rpx; }
|
||||
.remove {
|
||||
position: absolute; top: -8rpx; right: -8rpx;
|
||||
width: 36rpx; height: 36rpx; background: #ef4444; color: #fff;
|
||||
border-radius: 50%; text-align: center; line-height: 36rpx; font-size: 28rpx;
|
||||
}
|
||||
.add-btn {
|
||||
width: 160rpx; height: 160rpx; border: 2rpx dashed #ddd; border-radius: 12rpx;
|
||||
display: flex; align-items: center; justify-content: center; background: #fafafa;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<view class="store-picker">
|
||||
<view class="picker-row" @click="openPicker">
|
||||
<text class="label">所属诊所</text>
|
||||
<text class="value" :class="{ placeholder: !selectedLabel }">{{ selectedLabel || '请选择诊所' }}</text>
|
||||
<u-icon name="arrow-right" size="28" color="#999" />
|
||||
</view>
|
||||
<u-popup v-model="visible" mode="bottom" border-radius="24" height="70%">
|
||||
<view class="popup-inner">
|
||||
<view class="popup-title">选择诊所</view>
|
||||
<input class="search-input" v-model="keyword" placeholder="搜索诊所名称" @input="onSearch" />
|
||||
<scroll-view scroll-y class="option-list">
|
||||
<view
|
||||
v-for="item in options"
|
||||
:key="item.id"
|
||||
class="option-item"
|
||||
@click="select(item)"
|
||||
>
|
||||
<text>{{ item.name }}</text>
|
||||
<text v-if="item.position" class="sub">{{ item.position }}</text>
|
||||
</view>
|
||||
<view v-if="!loading && !options.length" class="empty">暂无诊所</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</u-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'StoreSearchPicker',
|
||||
props: {
|
||||
value: { type: [Number, String], default: 0 },
|
||||
api: { type: Object, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
keyword: '',
|
||||
options: [],
|
||||
loading: false,
|
||||
selectedLabel: '',
|
||||
searchTimer: null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
openPicker() {
|
||||
this.visible = true;
|
||||
this.fetchOptions('');
|
||||
},
|
||||
onSearch() {
|
||||
if (this.searchTimer) clearTimeout(this.searchTimer);
|
||||
this.searchTimer = setTimeout(() => this.fetchOptions(this.keyword.trim()), 300);
|
||||
},
|
||||
async fetchOptions(keyword) {
|
||||
this.loading = true;
|
||||
try {
|
||||
const wrap = await this.api.getPlatformStoreOptions(keyword);
|
||||
this.options = (wrap && wrap.ok && wrap.data) ? wrap.data : [];
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
select(item) {
|
||||
this.selectedLabel = item.position ? `${item.name}(${item.position})` : item.name;
|
||||
this.$emit('input', item.id);
|
||||
this.visible = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.picker-row {
|
||||
display: flex; align-items: center; padding: 24rpx 0;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
.label { width: 180rpx; font-size: 28rpx; color: #333; }
|
||||
.value { flex: 1; font-size: 28rpx; text-align: right; margin-right: 8rpx; }
|
||||
.placeholder { color: #bbb; }
|
||||
.popup-inner { padding: 32rpx; height: 100%; display: flex; flex-direction: column; }
|
||||
.popup-title { font-size: 32rpx; font-weight: 600; margin-bottom: 24rpx; }
|
||||
.search-input {
|
||||
background: #f5f5f5; border-radius: 12rpx; padding: 16rpx 24rpx; margin-bottom: 16rpx;
|
||||
}
|
||||
.option-list { flex: 1; }
|
||||
.option-item {
|
||||
padding: 24rpx 0; border-bottom: 1rpx solid #f0f0f0;
|
||||
display: flex; flex-direction: column;
|
||||
}
|
||||
.sub { font-size: 24rpx; color: #999; margin-top: 8rpx; }
|
||||
.empty { text-align: center; color: #999; padding: 60rpx 0; }
|
||||
</style>
|
||||
175
subPackages/sub_salesperson_manage/drug-commission/index.vue
Normal file
175
subPackages/sub_salesperson_manage/drug-commission/index.vue
Normal file
@@ -0,0 +1,175 @@
|
||||
<template>
|
||||
<page-layout :is-platform="ctx.isPlatform" title="药品佣金配置" :show-back="true">
|
||||
<view class="page">
|
||||
<view class="toolbar">
|
||||
<input class="search" v-model="keyword" placeholder="搜索药品" @confirm="reload" />
|
||||
<view class="search-btn" @click="reload">搜索</view>
|
||||
</view>
|
||||
|
||||
<view v-if="salespersonId > 0" class="copy-row">
|
||||
<picker :range="copyLabels" :value="copyIndex" @change="onCopyFrom">
|
||||
<view class="copy-picker">{{ copyLabels[copyIndex] || '从其他推广员复制' }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view v-if="!list.length && !loading" class="empty">暂无药品</view>
|
||||
<view v-for="item in list" :key="item.drug_id" class="row">
|
||||
<view class="drug-info">
|
||||
<text class="drug-name">{{ item.drug_name }}</text>
|
||||
<text class="spec">{{ item.drug_spec }}</text>
|
||||
<text class="price">售价 ¥{{ item.price }} / 供货 ¥{{ item.buy_price }}</text>
|
||||
</view>
|
||||
<input
|
||||
class="commission-input"
|
||||
v-model="dirtyMap[item.drug_id]"
|
||||
type="digit"
|
||||
placeholder="元/件"
|
||||
@input="onCommissionInput(item.drug_id, $event)"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view v-if="salespersonId > 0" class="save-btn" @click="save">保存修改</view>
|
||||
<view v-else class="hint">请先保存推广员基本信息后再配置药品佣金</view>
|
||||
</view>
|
||||
</page-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PageLayout from '../components/PageLayout.vue';
|
||||
import { getSalespersonManageContext, resolveManageMode } from '../common/context.js';
|
||||
|
||||
export default {
|
||||
components: { PageLayout },
|
||||
data() {
|
||||
return {
|
||||
ctx: getSalespersonManageContext('clinic'),
|
||||
salespersonId: 0,
|
||||
storeId: 0,
|
||||
keyword: '',
|
||||
list: [],
|
||||
loading: false,
|
||||
dirtyMap: {},
|
||||
copyOptions: [],
|
||||
copyIndex: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
copyLabels() {
|
||||
return ['从其他推广员复制', ...this.copyOptions.map((o) => o.label)];
|
||||
},
|
||||
},
|
||||
onLoad(options) {
|
||||
this.ctx = getSalespersonManageContext(resolveManageMode(options));
|
||||
this.salespersonId = Number(options.salesperson_id || 0);
|
||||
this.storeId = Number(options.store_id || 0);
|
||||
this.reload();
|
||||
if (this.salespersonId > 0) this.loadCopyOptions();
|
||||
},
|
||||
methods: {
|
||||
async reload() {
|
||||
if (!this.salespersonId) return;
|
||||
this.loading = true;
|
||||
try {
|
||||
const params = {
|
||||
salesperson_id: this.salespersonId,
|
||||
keyword: this.keyword,
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
};
|
||||
if (this.storeId > 0) params.store_id = this.storeId;
|
||||
const wrap = await this.ctx.api.getDrugCommissionList(params);
|
||||
const items = (wrap && wrap.ok && wrap.data && wrap.data.items) ? wrap.data.items : [];
|
||||
this.list = items;
|
||||
const map = { ...this.dirtyMap };
|
||||
items.forEach((row) => {
|
||||
if (map[row.drug_id] === undefined) {
|
||||
map[row.drug_id] = row.commission || '';
|
||||
}
|
||||
});
|
||||
this.dirtyMap = map;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
async loadCopyOptions() {
|
||||
const wrap = await this.ctx.api.getList({ page: 1, pageSize: 100 });
|
||||
if (!wrap || !wrap.ok) return;
|
||||
this.copyOptions = (wrap.data.items || [])
|
||||
.filter((sp) => Number(sp.id) !== this.salespersonId)
|
||||
.map((sp) => ({
|
||||
label: sp.nick_name || sp.phone || `推广员#${sp.id}`,
|
||||
value: Number(sp.id),
|
||||
}));
|
||||
},
|
||||
onCommissionInput(drugId, e) {
|
||||
this.$set(this.dirtyMap, drugId, e.detail.value);
|
||||
},
|
||||
async onCopyFrom(e) {
|
||||
const idx = Number(e.detail.value);
|
||||
this.copyIndex = idx;
|
||||
if (idx <= 0 || !this.copyOptions[idx - 1]) return;
|
||||
const fromId = this.copyOptions[idx - 1].value;
|
||||
const wrap = await this.ctx.api.getDrugCommissionCopyFrom(fromId);
|
||||
if (!wrap || !wrap.ok) return;
|
||||
const items = wrap.data.items || [];
|
||||
const map = { ...this.dirtyMap };
|
||||
items.forEach((row) => {
|
||||
map[row.drug_id] = row.commission || '';
|
||||
});
|
||||
this.dirtyMap = map;
|
||||
uni.showToast({ title: '已复制', icon: 'success' });
|
||||
},
|
||||
async save() {
|
||||
const items = Object.keys(this.dirtyMap)
|
||||
.filter((drugId) => this.dirtyMap[drugId] !== '' && this.dirtyMap[drugId] != null)
|
||||
.map((drugId) => ({
|
||||
drug_id: Number(drugId),
|
||||
commission: String(this.dirtyMap[drugId]),
|
||||
}));
|
||||
if (!items.length) {
|
||||
uni.showToast({ title: '无修改', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
uni.showLoading({ title: '保存中' });
|
||||
try {
|
||||
const wrap = await this.ctx.api.saveDrugCommission({
|
||||
salesperson_id: this.salespersonId,
|
||||
items,
|
||||
});
|
||||
if (wrap && wrap.ok) {
|
||||
uni.showToast({ title: '保存成功', icon: 'success' });
|
||||
}
|
||||
} finally {
|
||||
uni.hideLoading();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page { padding: 24rpx; padding-bottom: 120rpx; }
|
||||
.toolbar { display: flex; gap: 12rpx; margin-bottom: 16rpx; }
|
||||
.search { flex: 1; background: #fff; border-radius: 12rpx; padding: 16rpx 20rpx; font-size: 26rpx; }
|
||||
.search-btn { background: #6ACDBB; color: #fff; padding: 16rpx 24rpx; border-radius: 12rpx; font-size: 26rpx; }
|
||||
.copy-row { margin-bottom: 16rpx; }
|
||||
.copy-picker { background: #fff; padding: 20rpx; border-radius: 12rpx; font-size: 26rpx; color: #6ACDBB; }
|
||||
.row {
|
||||
display: flex; align-items: center; background: #fff; border-radius: 16rpx;
|
||||
padding: 20rpx; margin-bottom: 12rpx;
|
||||
}
|
||||
.drug-info { flex: 1; }
|
||||
.drug-name { display: block; font-size: 28rpx; font-weight: 600; }
|
||||
.spec, .price { font-size: 22rpx; color: #999; display: block; margin-top: 4rpx; }
|
||||
.commission-input {
|
||||
width: 140rpx; text-align: center; background: #f5f5f5; border-radius: 8rpx;
|
||||
padding: 12rpx; font-size: 26rpx;
|
||||
}
|
||||
.save-btn {
|
||||
position: fixed; left: 24rpx; right: 24rpx; bottom: 48rpx;
|
||||
background: linear-gradient(90deg, #00BFA6, #6ACDBB); color: #fff;
|
||||
text-align: center; padding: 24rpx; border-radius: 16rpx; font-size: 30rpx;
|
||||
}
|
||||
.hint { text-align: center; color: #999; padding: 40rpx; }
|
||||
.empty { text-align: center; color: #999; padding: 80rpx 0; }
|
||||
</style>
|
||||
172
subPackages/sub_salesperson_manage/form.vue
Normal file
172
subPackages/sub_salesperson_manage/form.vue
Normal file
@@ -0,0 +1,172 @@
|
||||
<template>
|
||||
<page-layout :is-platform="ctx.isPlatform" :title="isUpdate ? '编辑推广员' : '新增推广员'" :show-back="true">
|
||||
<view class="page">
|
||||
<store-search-picker v-if="ctx.isPlatform && !isUpdate" v-model="form.store_id" :api="ctx.api" />
|
||||
|
||||
<view class="field">
|
||||
<text class="label">手机号</text>
|
||||
<input class="input" v-model="form.phone" type="number" maxlength="11" placeholder="请输入手机号" :disabled="isUpdate" />
|
||||
</view>
|
||||
<view class="field">
|
||||
<text class="label">名称</text>
|
||||
<input class="input" v-model="form.nick_name" placeholder="请输入名称" />
|
||||
</view>
|
||||
|
||||
<view class="field">
|
||||
<text class="label">西药分成类型</text>
|
||||
<picker :range="splitTypeLabels" :value="splitTypeIndex" @change="onSplitType">
|
||||
<view class="picker-val">{{ splitTypeLabels[splitTypeIndex] }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view v-if="Number(form.split_type) === 1" class="field">
|
||||
<text class="label">分成比例(%)</text>
|
||||
<input class="input" v-model="form.split" type="digit" placeholder="0-100" />
|
||||
</view>
|
||||
<view v-else class="field link" @click="goDrugCommission">
|
||||
<text class="label">药品佣金配置</text>
|
||||
<text class="link-text">去配置元/件分成 ></text>
|
||||
</view>
|
||||
|
||||
<view class="section-title">中药推广分成</view>
|
||||
<view class="field">
|
||||
<text class="label">中药分成(%)</text>
|
||||
<input class="input" v-model="form.tcm_split" type="digit" placeholder="0 表示不分成" />
|
||||
</view>
|
||||
<view v-if="Number(form.tcm_split) > 0" class="field">
|
||||
<text class="label">中药分成基数</text>
|
||||
<picker :range="tcmBaseLabels" :value="tcmBaseIndex" @change="onTcmBase">
|
||||
<view class="picker-val">{{ tcmBaseLabels[tcmBaseIndex] }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="submit-btn" @click="submit">保存</view>
|
||||
</view>
|
||||
</page-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PageLayout from './components/PageLayout.vue';
|
||||
import StoreSearchPicker from './components/StoreSearchPicker.vue';
|
||||
import { getSalespersonManageContext, resolveManageMode } from './common/context.js';
|
||||
import { SPLIT_TYPE_OPTIONS, TCM_BASE_OPTIONS } from './common/labels.js';
|
||||
|
||||
const ROOT = '/subPackages/sub_salesperson_manage';
|
||||
|
||||
export default {
|
||||
components: { PageLayout, StoreSearchPicker },
|
||||
data() {
|
||||
return {
|
||||
ctx: getSalespersonManageContext('clinic'),
|
||||
isUpdate: false,
|
||||
form: {
|
||||
id: 0,
|
||||
store_id: 0,
|
||||
phone: '',
|
||||
nick_name: '',
|
||||
split_type: 0,
|
||||
split: 0,
|
||||
tcm_split: 0,
|
||||
tcm_base_type: 0,
|
||||
},
|
||||
splitTypeLabels: SPLIT_TYPE_OPTIONS.map((o) => o.label),
|
||||
tcmBaseLabels: TCM_BASE_OPTIONS.map((o) => o.label),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
splitTypeIndex() {
|
||||
return SPLIT_TYPE_OPTIONS.findIndex((o) => o.value === Number(this.form.split_type));
|
||||
},
|
||||
tcmBaseIndex() {
|
||||
return TCM_BASE_OPTIONS.findIndex((o) => o.value === Number(this.form.tcm_base_type));
|
||||
},
|
||||
},
|
||||
onLoad(options) {
|
||||
this.ctx = getSalespersonManageContext(resolveManageMode(options));
|
||||
this.isUpdate = !!options.id;
|
||||
if (options.id) {
|
||||
this.form.id = Number(options.id);
|
||||
this.form.store_id = Number(options.store_id || 0);
|
||||
this.loadDetail(options);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loadDetail(options) {
|
||||
const wrap = await this.ctx.api.getList({ page: 1, pageSize: 100 });
|
||||
if (!wrap || !wrap.ok) return;
|
||||
const item = (wrap.data.items || []).find((r) => Number(r.id) === Number(options.id));
|
||||
if (!item) return;
|
||||
this.form.phone = item.phone || '';
|
||||
this.form.nick_name = item.nick_name || '';
|
||||
this.form.split_type = Number(item.split_type) || 0;
|
||||
this.form.split = item.split || 0;
|
||||
this.form.tcm_split = item.tcm_split || 0;
|
||||
this.form.tcm_base_type = Number(item.tcm_base_type) || 0;
|
||||
if (!this.form.store_id && item.qr_code) {
|
||||
this.form.store_id = item.qr_code.store_id || 0;
|
||||
}
|
||||
},
|
||||
onSplitType(e) {
|
||||
const idx = Number(e.detail.value);
|
||||
this.form.split_type = SPLIT_TYPE_OPTIONS[idx].value;
|
||||
},
|
||||
onTcmBase(e) {
|
||||
const idx = Number(e.detail.value);
|
||||
this.form.tcm_base_type = TCM_BASE_OPTIONS[idx].value;
|
||||
},
|
||||
goDrugCommission() {
|
||||
const id = this.form.id || '';
|
||||
const storeId = this.form.store_id || '';
|
||||
uni.navigateTo({
|
||||
url: `${ROOT}/drug-commission/index?mode=${this.ctx.mode}&salesperson_id=${id}&store_id=${storeId}`,
|
||||
});
|
||||
},
|
||||
async submit() {
|
||||
if (!this.form.phone || !this.form.nick_name) {
|
||||
uni.showToast({ title: '请填写手机号和名称', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
if (this.ctx.isPlatform && !this.isUpdate && !this.form.store_id) {
|
||||
uni.showToast({ title: '请选择诊所', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
const payload = { ...this.form };
|
||||
if (Number(payload.split_type) === 0) payload.split = 0;
|
||||
|
||||
uni.showLoading({ title: '保存中' });
|
||||
try {
|
||||
const wrap = this.isUpdate
|
||||
? await this.ctx.api.update(payload)
|
||||
: await this.ctx.api.create(payload);
|
||||
if (wrap && wrap.ok) {
|
||||
if (!this.isUpdate && wrap.data && wrap.data.admin_account_created === false) {
|
||||
uni.showToast({ title: '已创建,请手动开通后台', icon: 'none', duration: 2500 });
|
||||
} else {
|
||||
uni.showToast({ title: '保存成功', icon: 'success' });
|
||||
}
|
||||
setTimeout(() => uni.navigateBack(), 500);
|
||||
}
|
||||
} finally {
|
||||
uni.hideLoading();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page { padding: 24rpx; }
|
||||
.field {
|
||||
display: flex; align-items: center; background: #fff; border-radius: 16rpx;
|
||||
padding: 24rpx; margin-bottom: 16rpx;
|
||||
}
|
||||
.field.link { justify-content: space-between; }
|
||||
.label { width: 200rpx; font-size: 28rpx; color: #333; flex-shrink: 0; }
|
||||
.input { flex: 1; font-size: 28rpx; text-align: right; }
|
||||
.picker-val { flex: 1; font-size: 28rpx; text-align: right; color: #333; }
|
||||
.link-text { font-size: 26rpx; color: #6ACDBB; }
|
||||
.section-title { font-size: 26rpx; color: #999; margin: 24rpx 0 12rpx 8rpx; }
|
||||
.submit-btn {
|
||||
margin-top: 40rpx; background: linear-gradient(90deg, #00BFA6, #6ACDBB);
|
||||
color: #fff; text-align: center; padding: 24rpx; border-radius: 16rpx; font-size: 30rpx;
|
||||
}
|
||||
</style>
|
||||
226
subPackages/sub_salesperson_manage/index.vue
Normal file
226
subPackages/sub_salesperson_manage/index.vue
Normal file
@@ -0,0 +1,226 @@
|
||||
<template>
|
||||
<page-layout :is-platform="ctx.isPlatform" title="推广员管理" :show-back="true">
|
||||
<view class="page">
|
||||
<view v-if="ctx.isPlatform" class="filter-panel">
|
||||
<input class="filter-input" v-model="filters.store_keyword" placeholder="诊所名称" />
|
||||
<input class="filter-input" v-model="filters.nick_name" placeholder="推广员名称" />
|
||||
<input class="filter-input" v-model="filters.phone" placeholder="手机号" />
|
||||
<view class="filter-btn" @click="reload">查询</view>
|
||||
</view>
|
||||
|
||||
<view v-if="!ctx.isPlatform" class="see-price-row">
|
||||
<text>推广员可见价格</text>
|
||||
<switch :checked="seePrice === 1" @change="onToggleSeePrice" color="#6ACDBB" />
|
||||
</view>
|
||||
|
||||
<view class="add-bar">
|
||||
<view class="add-btn" @click="goCreate">+ 新增推广员</view>
|
||||
</view>
|
||||
|
||||
<view v-if="loading" class="loading-tip">加载中...</view>
|
||||
<view v-else-if="!list.length" class="empty">暂无推广员</view>
|
||||
|
||||
<view v-for="item in list" :key="item.id" class="card">
|
||||
<view class="card-head">
|
||||
<view>
|
||||
<text class="name">{{ item.nick_name || '未知' }}</text>
|
||||
<text v-if="item.store_name" class="store">{{ item.store_name }}</text>
|
||||
</view>
|
||||
<view class="tags">
|
||||
<text class="tag">{{ splitLabel(item.split_type) }}</text>
|
||||
<text v-if="Number(item.tcm_split) > 0" class="tag tcm">中药 {{ item.tcm_split }}%</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<text class="phone">{{ item.phone }}</text>
|
||||
<view class="stat-row">
|
||||
<text>累计获客</text>
|
||||
<text class="num">{{ (item.count && item.count.user_number) || 0 }}</text>
|
||||
</view>
|
||||
<view class="stat-row">
|
||||
<text>待结算</text>
|
||||
<text class="num pending">¥{{ (item.count && item.count.pending_amount) || '0.00' }}</text>
|
||||
</view>
|
||||
<view class="stat-row">
|
||||
<text>后台账号</text>
|
||||
<text :class="item.has_admin_account ? 'ok' : 'warn'">{{ item.has_admin_account ? '已开通' : '未开通' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="actions">
|
||||
<view class="action-btn" @click="goEdit(item)">编辑</view>
|
||||
<view class="action-btn primary" @click="goCommission(item)">分成与结算</view>
|
||||
<view v-if="item.qr_code && item.qr_code.qr_code" class="action-btn" @click="openQr(item)">二维码</view>
|
||||
<view class="action-btn" @click="goLeads(item)">获客</view>
|
||||
<view
|
||||
v-if="!item.has_admin_account"
|
||||
class="action-btn warn"
|
||||
@click="openAdminAccount(item)"
|
||||
>开通后台</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</page-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PageLayout from './components/PageLayout.vue';
|
||||
import { getSalespersonManageContext, resolveManageMode } from './common/context.js';
|
||||
import { splitTypeLabel } from './common/labels.js';
|
||||
import { openQrCodePreview } from '@/utils/qrCodePreview.js';
|
||||
|
||||
const ROOT = '/subPackages/sub_salesperson_manage';
|
||||
|
||||
export default {
|
||||
components: { PageLayout },
|
||||
data() {
|
||||
return {
|
||||
ctx: getSalespersonManageContext('clinic'),
|
||||
filters: { store_keyword: '', nick_name: '', phone: '' },
|
||||
presetStoreId: 0,
|
||||
list: [],
|
||||
loading: false,
|
||||
seePrice: 0,
|
||||
openingId: 0,
|
||||
};
|
||||
},
|
||||
onLoad(options) {
|
||||
this.ctx = getSalespersonManageContext(resolveManageMode(options));
|
||||
this.presetStoreId = Number(options.store_id || 0);
|
||||
if (this.presetStoreId > 0) {
|
||||
this.filters.store_keyword = '';
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
this.reload();
|
||||
if (!this.ctx.isPlatform) this.loadSeePrice();
|
||||
},
|
||||
methods: {
|
||||
splitLabel: splitTypeLabel,
|
||||
async reload() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const params = {
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
...this.filters,
|
||||
};
|
||||
if (this.presetStoreId > 0) params.store_id = this.presetStoreId;
|
||||
const wrap = await this.ctx.api.getList(params);
|
||||
this.list = (wrap && wrap.ok && wrap.data && wrap.data.items) ? wrap.data.items : [];
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
async loadSeePrice() {
|
||||
const wrap = await this.ctx.api.getSeePriceDetail();
|
||||
if (wrap && wrap.ok && wrap.data) {
|
||||
this.seePrice = Number(wrap.data.see_price) || 0;
|
||||
}
|
||||
},
|
||||
async onToggleSeePrice(e) {
|
||||
const wrap = await this.ctx.api.toggleSeePrice();
|
||||
if (wrap && wrap.ok && wrap.data) {
|
||||
this.seePrice = Number(wrap.data.see_price) || 0;
|
||||
uni.showToast({ title: this.seePrice === 1 ? '已开启可见价格' : '已关闭可见价格', icon: 'none' });
|
||||
}
|
||||
},
|
||||
goCreate() {
|
||||
uni.navigateTo({ url: `${ROOT}/form?mode=${this.ctx.mode}` });
|
||||
},
|
||||
goEdit(item) {
|
||||
uni.navigateTo({
|
||||
url: `${ROOT}/form?mode=${this.ctx.mode}&id=${item.id}&store_id=${(item.qr_code && item.qr_code.store_id) || ''}`,
|
||||
});
|
||||
},
|
||||
goCommission(item) {
|
||||
const storeId = (item.qr_code && item.qr_code.store_id) || '';
|
||||
uni.navigateTo({
|
||||
url: `${ROOT}/commission/index?mode=${this.ctx.mode}&salesperson_id=${item.id}&nick_name=${encodeURIComponent(item.nick_name || '')}&store_id=${storeId}`,
|
||||
});
|
||||
},
|
||||
goLeads(item) {
|
||||
uni.navigateTo({
|
||||
url: `${ROOT}/leads/index?mode=${this.ctx.mode}&salesperson_id=${item.id}&nick_name=${encodeURIComponent(item.nick_name || '')}`,
|
||||
});
|
||||
},
|
||||
openQr(item) {
|
||||
openQrCodePreview({
|
||||
type: 'salesperson',
|
||||
title: item.nick_name || '推广员二维码',
|
||||
payload: {
|
||||
nick_name: item.nick_name || '',
|
||||
qr_code: {
|
||||
qr_code: (item.qr_code && item.qr_code.qr_code) || '',
|
||||
store: { name: item.store_name || '' },
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
async openAdminAccount(item) {
|
||||
if (this.openingId) return;
|
||||
this.openingId = item.id;
|
||||
try {
|
||||
const wrap = await this.ctx.api.openAdminAccount({
|
||||
salesperson_id: item.id,
|
||||
store_id: item.qr_code && item.qr_code.store_id,
|
||||
});
|
||||
if (wrap && wrap.ok) {
|
||||
uni.showModal({
|
||||
title: '开通成功',
|
||||
content: '默认密码:Xk123456@',
|
||||
showCancel: false,
|
||||
});
|
||||
this.reload();
|
||||
}
|
||||
} finally {
|
||||
this.openingId = 0;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page { padding: 24rpx; padding-bottom: 48rpx; }
|
||||
.filter-panel { display: flex; flex-wrap: wrap; gap: 12rpx; margin-bottom: 20rpx; }
|
||||
.filter-input {
|
||||
flex: 1; min-width: 200rpx; background: #fff; border-radius: 12rpx;
|
||||
padding: 16rpx 20rpx; font-size: 26rpx;
|
||||
}
|
||||
.filter-btn {
|
||||
background: #6ACDBB; color: #fff; padding: 16rpx 32rpx; border-radius: 12rpx; font-size: 26rpx;
|
||||
}
|
||||
.see-price-row {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
background: #fff; border-radius: 16rpx; padding: 24rpx; margin-bottom: 20rpx; font-size: 28rpx;
|
||||
}
|
||||
.add-bar { margin-bottom: 20rpx; }
|
||||
.add-btn {
|
||||
background: linear-gradient(90deg, #00BFA6, #6ACDBB); color: #fff;
|
||||
text-align: center; padding: 20rpx; border-radius: 16rpx; font-size: 28rpx;
|
||||
}
|
||||
.loading-tip, .empty { text-align: center; color: #999; padding: 80rpx 0; }
|
||||
.card {
|
||||
background: #fff; border-radius: 20rpx; padding: 28rpx; margin-bottom: 20rpx;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.04);
|
||||
}
|
||||
.card-head { display: flex; justify-content: space-between; margin-bottom: 16rpx; }
|
||||
.name { display: block; font-size: 32rpx; font-weight: 600; }
|
||||
.store { font-size: 24rpx; color: #999; }
|
||||
.tags { display: flex; flex-direction: column; gap: 8rpx; align-items: flex-end; }
|
||||
.tag { font-size: 22rpx; background: #EFF6FF; color: #3B82F6; padding: 4rpx 12rpx; border-radius: 8rpx; }
|
||||
.tag.tcm { background: #F0FDF4; color: #22C55E; }
|
||||
.phone { font-size: 26rpx; color: #666; display: block; margin-bottom: 12rpx; }
|
||||
.stat-row { display: flex; justify-content: space-between; font-size: 26rpx; margin-bottom: 8rpx; }
|
||||
.num { font-weight: 600; }
|
||||
.pending { color: #F97316; }
|
||||
.ok { color: #22C55E; }
|
||||
.warn { color: #EF4444; }
|
||||
.actions { display: flex; flex-wrap: wrap; gap: 12rpx; margin-top: 20rpx; }
|
||||
.action-btn {
|
||||
font-size: 24rpx; padding: 10rpx 20rpx; border-radius: 10rpx;
|
||||
background: #f5f5f5; color: #333;
|
||||
}
|
||||
.action-btn.primary { background: #6ACDBB; color: #fff; }
|
||||
.action-btn.warn { background: #FEF3C7; color: #D97706; }
|
||||
</style>
|
||||
71
subPackages/sub_salesperson_manage/leads/index.vue
Normal file
71
subPackages/sub_salesperson_manage/leads/index.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<page-layout :is-platform="ctx.isPlatform" :title="pageTitle" :show-back="true">
|
||||
<view class="page">
|
||||
<view v-if="!list.length && !loading" class="empty">暂无获客记录</view>
|
||||
<view v-for="item in list" :key="item.id" class="row">
|
||||
<view class="user">
|
||||
<text class="name">{{ (item.user && item.user.nickname) || '用户' }}</text>
|
||||
<text class="mobile">{{ (item.user && item.user.mobile) || '' }}</text>
|
||||
</view>
|
||||
<text class="time">{{ item.created_at_text || '' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</page-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PageLayout from '../components/PageLayout.vue';
|
||||
import { getSalespersonManageContext, resolveManageMode } from '../common/context.js';
|
||||
|
||||
export default {
|
||||
components: { PageLayout },
|
||||
data() {
|
||||
return {
|
||||
ctx: getSalespersonManageContext('clinic'),
|
||||
salespersonId: 0,
|
||||
nickName: '',
|
||||
list: [],
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
pageTitle() {
|
||||
return this.nickName ? `获客 - ${this.nickName}` : '获客记录';
|
||||
},
|
||||
},
|
||||
onLoad(options) {
|
||||
this.ctx = getSalespersonManageContext(resolveManageMode(options));
|
||||
this.salespersonId = Number(options.salesperson_id || 0);
|
||||
this.nickName = decodeURIComponent(options.nick_name || '');
|
||||
this.loadList();
|
||||
},
|
||||
methods: {
|
||||
async loadList() {
|
||||
if (!this.salespersonId) return;
|
||||
this.loading = true;
|
||||
try {
|
||||
const wrap = await this.ctx.api.getUserBindList({
|
||||
salesperson_id: this.salespersonId,
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
});
|
||||
this.list = (wrap && wrap.ok && wrap.data && wrap.data.items) ? wrap.data.items : [];
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page { padding: 24rpx; }
|
||||
.empty { text-align: center; color: #999; padding: 80rpx 0; }
|
||||
.row {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
background: #fff; border-radius: 16rpx; padding: 24rpx; margin-bottom: 12rpx;
|
||||
}
|
||||
.name { display: block; font-size: 28rpx; font-weight: 600; }
|
||||
.mobile { font-size: 24rpx; color: #999; }
|
||||
.time { font-size: 24rpx; color: #bbb; }
|
||||
</style>
|
||||
122
subPackages/sub_salesperson_manage/settlement/confirm.vue
Normal file
122
subPackages/sub_salesperson_manage/settlement/confirm.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<page-layout :is-platform="ctx.isPlatform" title="确认结算" :show-back="true">
|
||||
<view class="page">
|
||||
<view class="preview-card">
|
||||
<text>共 {{ recordCount }} 条明细</text>
|
||||
<text class="amount">结算金额 ¥{{ amount }}</text>
|
||||
</view>
|
||||
|
||||
<proof-image-uploader v-model="proofImages" />
|
||||
|
||||
<view class="field">
|
||||
<text class="label">备注</text>
|
||||
<input class="input" v-model="remark" placeholder="选填" />
|
||||
</view>
|
||||
|
||||
<view class="submit-btn" @click="confirm">确认结算</view>
|
||||
</view>
|
||||
</page-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PageLayout from '../components/PageLayout.vue';
|
||||
import ProofImageUploader from '../components/ProofImageUploader.vue';
|
||||
import { getSalespersonManageContext, resolveManageMode } from '../common/context.js';
|
||||
|
||||
export default {
|
||||
components: { PageLayout, ProofImageUploader },
|
||||
data() {
|
||||
return {
|
||||
ctx: getSalespersonManageContext('clinic'),
|
||||
salespersonId: 0,
|
||||
storeId: 0,
|
||||
settlementType: 1,
|
||||
periodStart: 0,
|
||||
periodEnd: 0,
|
||||
orderIds: [],
|
||||
amount: '0.00',
|
||||
recordCount: 0,
|
||||
proofImages: [],
|
||||
remark: '',
|
||||
};
|
||||
},
|
||||
onLoad(options) {
|
||||
this.ctx = getSalespersonManageContext(resolveManageMode(options));
|
||||
this.salespersonId = Number(options.salesperson_id || 0);
|
||||
this.storeId = Number(options.store_id || 0);
|
||||
this.settlementType = Number(options.settlement_type || 1);
|
||||
this.periodStart = Number(options.period_start || 0);
|
||||
this.periodEnd = Number(options.period_end || 0);
|
||||
this.amount = decodeURIComponent(options.amount || '0.00');
|
||||
this.recordCount = Number(options.record_count || 0);
|
||||
if (options.order_ids) {
|
||||
this.orderIds = String(options.order_ids).split(',').map(Number).filter(Boolean);
|
||||
}
|
||||
if (!this.recordCount || !this.amount || this.amount === '0.00') {
|
||||
this.loadPreview();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
buildParams() {
|
||||
const params = {
|
||||
salesperson_id: this.salespersonId,
|
||||
settlement_type: this.settlementType,
|
||||
};
|
||||
if (this.storeId > 0) params.store_id = this.storeId;
|
||||
if (this.settlementType === 1) {
|
||||
params.period_start = this.periodStart;
|
||||
params.period_end = this.periodEnd;
|
||||
} else {
|
||||
params.order_ids = this.orderIds.join(',');
|
||||
}
|
||||
return params;
|
||||
},
|
||||
async loadPreview() {
|
||||
const wrap = await this.ctx.api.getSettlementPreview(this.buildParams());
|
||||
if (wrap && wrap.ok && wrap.data) {
|
||||
this.amount = wrap.data.amount || '0.00';
|
||||
this.recordCount = wrap.data.record_count || 0;
|
||||
}
|
||||
},
|
||||
async confirm() {
|
||||
if (!this.proofImages.length) {
|
||||
uni.showToast({ title: '请上传至少1张结算凭证', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
uni.showLoading({ title: '结算中' });
|
||||
try {
|
||||
const wrap = await this.ctx.api.confirmSettlement({
|
||||
...this.buildParams(),
|
||||
proof_images: this.proofImages,
|
||||
remark: this.remark.trim(),
|
||||
});
|
||||
if (wrap && wrap.ok) {
|
||||
uni.showToast({ title: '结算成功', icon: 'success' });
|
||||
setTimeout(() => uni.navigateBack({ delta: 2 }), 500);
|
||||
}
|
||||
} finally {
|
||||
uni.hideLoading();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page { padding: 24rpx; }
|
||||
.preview-card {
|
||||
background: #FFF7ED; border-radius: 16rpx; padding: 28rpx; margin-bottom: 24rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
.amount { display: block; font-size: 36rpx; font-weight: 700; color: #F97316; margin-top: 12rpx; }
|
||||
.field {
|
||||
display: flex; align-items: center; background: #fff; border-radius: 16rpx;
|
||||
padding: 24rpx; margin-top: 24rpx;
|
||||
}
|
||||
.label { width: 100rpx; font-size: 28rpx; }
|
||||
.input { flex: 1; font-size: 28rpx; text-align: right; }
|
||||
.submit-btn {
|
||||
margin-top: 48rpx; background: linear-gradient(90deg, #00BFA6, #6ACDBB); color: #fff;
|
||||
text-align: center; padding: 24rpx; border-radius: 16rpx; font-size: 30rpx;
|
||||
}
|
||||
</style>
|
||||
97
subPackages/sub_salesperson_manage/settlement/detail.vue
Normal file
97
subPackages/sub_salesperson_manage/settlement/detail.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<page-layout :is-platform="ctx.isPlatform" title="结算详情" :show-back="true">
|
||||
<view class="page" v-if="detail">
|
||||
<view class="card">
|
||||
<text class="no">{{ detail.settlement_no }}</text>
|
||||
<view class="row"><text>结算方式</text><text>{{ detail.settlement_type_text }}</text></view>
|
||||
<view class="row"><text>结算金额</text><text class="amount">¥{{ detail.amount }}</text></view>
|
||||
<view class="row"><text>明细数</text><text>{{ detail.record_count }}</text></view>
|
||||
<view class="row"><text>时间</text><text>{{ detail.created_at_text }}</text></view>
|
||||
<view v-if="detail.remark" class="row"><text>备注</text><text>{{ detail.remark }}</text></view>
|
||||
</view>
|
||||
|
||||
<view v-if="proofImages.length" class="proof-section">
|
||||
<text class="section-title">结算凭证</text>
|
||||
<view class="proof-list">
|
||||
<image
|
||||
v-for="(url, idx) in proofImages"
|
||||
:key="url"
|
||||
class="proof-img"
|
||||
:src="url"
|
||||
mode="aspectFill"
|
||||
@click="previewProof(idx)"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="lines.length" class="lines-section">
|
||||
<text class="section-title">明细</text>
|
||||
<view v-for="(line, idx) in lines" :key="idx" class="line-row">
|
||||
<text class="line-order">{{ line.order_no }}</text>
|
||||
<text class="line-drug">{{ line.drug_info }}</text>
|
||||
<text class="line-amount">¥{{ line.commission_amount }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</page-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PageLayout from '../components/PageLayout.vue';
|
||||
import { getSalespersonManageContext, resolveManageMode } from '../common/context.js';
|
||||
|
||||
export default {
|
||||
components: { PageLayout },
|
||||
data() {
|
||||
return {
|
||||
ctx: getSalespersonManageContext('clinic'),
|
||||
detail: null,
|
||||
lines: [],
|
||||
proofImages: [],
|
||||
};
|
||||
},
|
||||
onLoad(options) {
|
||||
this.ctx = getSalespersonManageContext(resolveManageMode(options));
|
||||
this.loadDetail(Number(options.id || 0), Number(options.store_id || 0));
|
||||
},
|
||||
methods: {
|
||||
async loadDetail(id, storeId) {
|
||||
if (!id) return;
|
||||
const params = { id };
|
||||
if (storeId > 0) params.store_id = storeId;
|
||||
const wrap = await this.ctx.api.getSettlementDetail(params);
|
||||
if (!wrap || !wrap.ok) return;
|
||||
this.detail = wrap.data;
|
||||
this.lines = (wrap.data && wrap.data.lines) ? wrap.data.lines : (wrap.data.records || []);
|
||||
const proofs = wrap.data && wrap.data.proof_images;
|
||||
if (Array.isArray(proofs)) {
|
||||
this.proofImages = proofs;
|
||||
} else if (typeof proofs === 'string' && proofs) {
|
||||
try { this.proofImages = JSON.parse(proofs); } catch (e) { this.proofImages = [proofs]; }
|
||||
} else {
|
||||
this.proofImages = [];
|
||||
}
|
||||
},
|
||||
previewProof(idx) {
|
||||
uni.previewImage({ urls: this.proofImages, current: this.proofImages[idx] });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page { padding: 24rpx; }
|
||||
.card { background: #fff; border-radius: 20rpx; padding: 28rpx; margin-bottom: 24rpx; }
|
||||
.no { display: block; font-size: 32rpx; font-weight: 600; margin-bottom: 20rpx; }
|
||||
.row { display: flex; justify-content: space-between; font-size: 26rpx; margin-bottom: 12rpx; color: #666; }
|
||||
.amount { color: #F97316; font-weight: 600; }
|
||||
.section-title { font-size: 28rpx; font-weight: 600; margin-bottom: 16rpx; display: block; }
|
||||
.proof-list { display: flex; flex-wrap: wrap; gap: 16rpx; margin-bottom: 24rpx; }
|
||||
.proof-img { width: 200rpx; height: 200rpx; border-radius: 12rpx; }
|
||||
.line-row {
|
||||
background: #fff; border-radius: 12rpx; padding: 20rpx; margin-bottom: 12rpx;
|
||||
}
|
||||
.line-order { display: block; font-size: 26rpx; font-weight: 600; }
|
||||
.line-drug { font-size: 24rpx; color: #666; display: block; margin: 8rpx 0; }
|
||||
.line-amount { font-size: 26rpx; color: #F97316; }
|
||||
</style>
|
||||
114
subPackages/sub_salesperson_manage/settlement/preview.vue
Normal file
114
subPackages/sub_salesperson_manage/settlement/preview.vue
Normal file
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<page-layout :is-platform="ctx.isPlatform" title="按时间段结算" :show-back="true">
|
||||
<view class="page">
|
||||
<view class="field">
|
||||
<text class="label">开始日期</text>
|
||||
<picker mode="date" :value="dateStart" @change="onStart">
|
||||
<view class="picker-val">{{ dateStart || '请选择' }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="field">
|
||||
<text class="label">结束日期</text>
|
||||
<picker mode="date" :value="dateEnd" @change="onEnd">
|
||||
<view class="picker-val">{{ dateEnd || '请选择' }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view v-if="preview" class="preview-card">
|
||||
<text>共 {{ preview.record_count }} 条明细</text>
|
||||
<text v-if="preview.order_count">涉及 {{ preview.order_count }} 个订单</text>
|
||||
<text class="amount">结算金额 ¥{{ preview.amount }}</text>
|
||||
</view>
|
||||
|
||||
<view class="submit-btn" @click="goConfirm">下一步:上传凭证</view>
|
||||
</view>
|
||||
</page-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PageLayout from '../components/PageLayout.vue';
|
||||
import { getSalespersonManageContext, resolveManageMode } from '../common/context.js';
|
||||
|
||||
const ROOT = '/subPackages/sub_salesperson_manage';
|
||||
|
||||
export default {
|
||||
components: { PageLayout },
|
||||
data() {
|
||||
return {
|
||||
ctx: getSalespersonManageContext('clinic'),
|
||||
salespersonId: 0,
|
||||
storeId: 0,
|
||||
dateStart: '',
|
||||
dateEnd: '',
|
||||
preview: null,
|
||||
};
|
||||
},
|
||||
onLoad(options) {
|
||||
this.ctx = getSalespersonManageContext(resolveManageMode(options));
|
||||
this.salespersonId = Number(options.salesperson_id || 0);
|
||||
this.storeId = Number(options.store_id || 0);
|
||||
},
|
||||
methods: {
|
||||
onStart(e) { this.dateStart = e.detail.value; },
|
||||
onEnd(e) { this.dateEnd = e.detail.value; },
|
||||
toUnixStart(dateStr) {
|
||||
return Math.floor(new Date(`${dateStr} 00:00:00`).getTime() / 1000);
|
||||
},
|
||||
toUnixEnd(dateStr) {
|
||||
return Math.floor(new Date(`${dateStr} 23:59:59`).getTime() / 1000);
|
||||
},
|
||||
async goConfirm() {
|
||||
if (!this.dateStart || !this.dateEnd) {
|
||||
uni.showToast({ title: '请选择时间段', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
const params = {
|
||||
salesperson_id: this.salespersonId,
|
||||
settlement_type: 1,
|
||||
period_start: this.toUnixStart(this.dateStart),
|
||||
period_end: this.toUnixEnd(this.dateEnd),
|
||||
};
|
||||
if (this.storeId > 0) params.store_id = this.storeId;
|
||||
|
||||
uni.showLoading({ title: '预览中' });
|
||||
try {
|
||||
const wrap = await this.ctx.api.getSettlementPreview(params);
|
||||
if (!wrap || !wrap.ok) return;
|
||||
this.preview = wrap.data;
|
||||
const q = [
|
||||
`mode=${this.ctx.mode}`,
|
||||
`salesperson_id=${this.salespersonId}`,
|
||||
`store_id=${this.storeId}`,
|
||||
'settlement_type=1',
|
||||
`period_start=${params.period_start}`,
|
||||
`period_end=${params.period_end}`,
|
||||
`amount=${encodeURIComponent(this.preview.amount || '')}`,
|
||||
`record_count=${this.preview.record_count || 0}`,
|
||||
].join('&');
|
||||
uni.navigateTo({ url: `${ROOT}/settlement/confirm?${q}` });
|
||||
} finally {
|
||||
uni.hideLoading();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page { padding: 24rpx; }
|
||||
.field {
|
||||
display: flex; align-items: center; background: #fff; border-radius: 16rpx;
|
||||
padding: 24rpx; margin-bottom: 16rpx;
|
||||
}
|
||||
.label { width: 160rpx; font-size: 28rpx; }
|
||||
.picker-val { flex: 1; text-align: right; font-size: 28rpx; }
|
||||
.preview-card {
|
||||
background: #FFF7ED; border-radius: 16rpx; padding: 28rpx; margin: 24rpx 0;
|
||||
display: flex; flex-direction: column; gap: 8rpx; font-size: 26rpx;
|
||||
}
|
||||
.amount { font-size: 36rpx; font-weight: 700; color: #F97316; margin-top: 8rpx; }
|
||||
.submit-btn {
|
||||
background: linear-gradient(90deg, #00BFA6, #6ACDBB); color: #fff;
|
||||
text-align: center; padding: 24rpx; border-radius: 16rpx; font-size: 30rpx; margin-top: 40rpx;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user