1. 修复了一些问题

This commit is contained in:
李琦
2026-06-09 17:02:13 +08:00
parent 9dd9b061c5
commit 3f6da79ece
38 changed files with 3814 additions and 343 deletions

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

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

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

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

View File

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

View File

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

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

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

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

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

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

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

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