1. 优化门店管理员的功能

This commit is contained in:
李琦
2026-06-02 08:20:44 +08:00
parent a515ed3d37
commit 777dd139a2
9 changed files with 909 additions and 29 deletions

View File

@@ -67,6 +67,10 @@ export function getLedgerDetail(params) {
return clinicRequest({ url: `${PREFIX}-settlement/ledger-detail`, method: 'GET', data: params });
}
export function getAccountChangeLogList(params) {
return clinicRequest({ url: `${PREFIX}-account-change-log/list`, method: 'GET', data: params });
}
export function getWithdrawalList(params) {
return clinicRequest({ url: `${PREFIX}-withdrawal/list`, method: 'GET', data: params });
}
@@ -115,6 +119,18 @@ export function getOrderDetail(id) {
return clinicRequest({ url: `${PREFIX}-order/detail`, method: 'GET', data: { id } });
}
export function getOrderTrace(params) {
return clinicRequest({ url: `${PREFIX}-order/trace`, method: 'GET', data: params });
}
export function getOrderLedgerDetail(params) {
return clinicRequest({ url: `${PREFIX}-order/ledger-detail`, method: 'GET', data: params });
}
export function getOrderReconciliationDetail(params) {
return clinicRequest({ url: `${PREFIX}-order/reconciliation-detail`, method: 'GET', data: params });
}
export function refundOrder(data) {
return clinicRequest({ url: `${PREFIX}-order/refund`, method: 'POST', data });
}

View File

@@ -412,6 +412,10 @@
"path": "withdrawal/settlement/detail",
"style": { "navigationBarTitleText": "结算明细", "navigationStyle": "custom" }
},
{
"path": "withdrawal/account-change/index",
"style": { "navigationBarTitleText": "资金变动", "navigationStyle": "custom" }
},
{
"path": "withdrawal/apply",
"style": { "navigationBarTitleText": "申请提现", "navigationStyle": "custom" }
@@ -436,6 +440,10 @@
"path": "order/detail",
"style": { "navigationBarTitleText": "订单详情", "navigationStyle": "custom" }
},
{
"path": "order/trace/index",
"style": { "navigationBarTitleText": "订单追溯", "navigationStyle": "custom" }
},
{
"path": "warehouse/index",
"style": { "navigationBarTitleText": "仓库管理", "navigationStyle": "custom" }

View File

@@ -56,15 +56,22 @@ export function mapWithdrawalRow(item) {
/** 结算记录列表行yii_ledger_log */
export function mapSettlementRow(item) {
if (!item) return {}
const isIncrease = item.merged
? (item.type_txt === '增加' || Number(item.amount) >= 0)
: item.type === 1
return {
id: item.id,
orderNo: item.order_no || (item.order_id ? '订单#' + item.order_id : '-'),
orderTypeText: item.order_type_txt || '',
feeTypeText: item.fee_type_txt || '-',
amountText: formatMoney(item.amount),
amountClass: ledgerTypeClass(item),
amountClass: isIncrease ? 'text-success' : 'text-danger',
typeText: item.type_txt || '-',
content: item.content || '',
createdAt: formatDateTime(item.created_at),
merged: !!item.merged,
orderId: item.order_id,
orderType: item.order_type,
_raw: item,
}
}
@@ -445,6 +452,26 @@ export function mapReconciliationStoreRow(item) {
}
}
/** 订单追溯对账汇总summary 字段,对齐 mapReconciliationStoreRow 结构) */
export function mapReconciliationOrderSummary(summary) {
if (!summary) return null
return {
totalSales: formatMoney(summary.total_sales_price),
totalSupply: formatMoney(summary.total_supply_price),
registrationPrice: formatMoney(summary.registration_price),
registerOrderNo: summary.register_order_no || '',
herbal: mapReconciliationCategory(summary, 'herbal'),
medicine: mapReconciliationCategory(summary, 'medicine'),
servicePackage: mapReconciliationCategory(summary, 'service_package'),
otherFees: {
express: formatMoney(summary.express_price),
process: formatMoney(summary.process_price),
treatment: formatMoney(summary.treatment_price),
},
_raw: summary,
}
}
export function mapReconciliationDrugRow(item) {
if (!item) return {}
return {

View File

@@ -122,11 +122,10 @@
:show="true"
/>
<!-- 底部悬浮操作栏移除确认发货全宽查看处方 -->
<view class="actions-bar" v-if="info.id && (canRefund || detailExtra.canViewPrescription)">
<!-- 退款按钮为次要操作 -->
<!-- 底部悬浮操作栏 -->
<view class="actions-bar" v-if="info.id">
<button class="btn-outline" @click="goTrace">订单追溯</button>
<button v-if="canRefund" class="btn-warn" @click="refund">申请退款</button>
<!-- 查看处方按钮为主操作利用 flex: 1 自动铺满剩余宽度 -->
<button v-if="detailExtra.canViewPrescription" class="btn-primary btn-full" @click="openPrescription">查看处方</button>
</view>
@@ -276,6 +275,11 @@ export default {
if (!pid) return
this.$refs.prescriptionPopup.open(pid)
},
goTrace() {
uni.navigateTo({
url: `/subPackages/sub_clinic_admin/order/trace/index?order_id=${this.id}&scene=product`,
})
},
},
}
</script>
@@ -547,6 +551,11 @@ $color-danger: #F53F3F;
background: $theme-primary;
color: #fff;
}
.btn-outline {
background: #fff;
color: $theme-primary;
border: 1rpx solid $theme-primary;
}
/* 次要操作(退款)给固定宽度 */
.btn-warn {

View File

@@ -0,0 +1,482 @@
<template>
<clinic-page-layout title="订单追溯" :show-back="true">
<view class="trace-container">
<view v-if="loading" class="loading-tip">加载中...</view>
<template v-else-if="traceData">
<!-- 摘要 -->
<view class="summary-card">
<view class="row"><text class="label">商品订单</text><text class="value">{{ summary.product_order_no || '—' }}</text></view>
<view class="row" v-if="summary.register_order_no"><text class="label">挂号订单</text><text class="value">{{ summary.register_order_no }}</text></view>
<view class="row" v-if="summary.prescription_no"><text class="label">处方号</text><text class="value">{{ summary.prescription_no }}</text></view>
<view class="row"><text class="label">结算状态</text><text class="value">{{ summary.is_settled === 1 ? '已结算' : '未结算' }}</text></view>
</view>
<!-- Tab -->
<view class="main-tabs">
<view
v-for="tab in mainTabs"
:key="tab.key"
class="main-tab"
:class="{ active: activeTab === tab.key }"
@click="switchMainTab(tab.key)"
>{{ tab.label }}</view>
</view>
<!-- 时间线 -->
<view v-show="activeTab === 'timeline'" class="tab-panel">
<view v-if="!timeline.length" class="empty">暂无时间线数据</view>
<view v-for="(node, idx) in timeline" :key="node.key || idx" class="timeline-item">
<view class="tl-dot" />
<view class="tl-body">
<view class="tl-title">{{ node.title }}</view>
<view class="tl-meta">
<text v-if="node.time">{{ node.time }}</text>
<text v-if="node.status" class="tl-status">{{ node.status }}</text>
</view>
<view v-if="node.extra" class="tl-extra">{{ node.extra }}</view>
</view>
</view>
</view>
<!-- 分账明细 -->
<view v-show="activeTab === 'ledger'" class="tab-panel">
<view v-if="ledgerLoading" class="loading-tip">加载分账...</view>
<template v-else>
<view v-if="hasRegisterLedger" class="sub-tabs">
<view
class="sub-tab"
:class="{ active: ledgerSubTab === 'product' }"
@click="switchLedgerSub('product')"
>商品订单</view>
<view
class="sub-tab"
:class="{ active: ledgerSubTab === 'register' }"
@click="switchLedgerSub('register')"
>挂号订单</view>
</view>
<view v-if="currentLedgerItems.length">
<view v-for="row in currentLedgerItems" :key="row._wxKey" class="detail-card">
<view class="row"><text class="label">费用类型</text><text class="value">{{ row.fee_type_txt || '-' }}</text></view>
<view class="row" v-if="row.drug_name"><text class="label">药品</text><text class="value">{{ row.drug_name }}</text></view>
<view class="row" v-if="row.specification"><text class="label">规格</text><text class="value">{{ row.specification }}</text></view>
<view class="row"><text class="label">分账金额</text><text class="value amount">{{ formatMoney(row.money) }}</text></view>
<view class="row"><text class="label">结算状态</text><text class="value">{{ row.status_txt || '-' }}</text></view>
<view class="row" v-if="row.created_at"><text class="label">时间</text><text class="value">{{ row.created_at }}</text></view>
</view>
</view>
<view v-else class="empty">暂无分账明细</view>
</template>
</view>
<!-- 对账明细 -->
<view v-show="activeTab === 'reconciliation'" class="tab-panel">
<view v-if="reconciliationLoading" class="loading-tip">加载对账...</view>
<template v-else>
<view v-if="reconciliationHint" class="hint-box">{{ reconciliationHint }}</view>
<template v-if="reconciliationSummary">
<view class="recon-summary-card">
<view class="recon-head">
<text class="recon-title">对账汇总</text>
<text v-if="reconciliationSummary.registrationPrice !== '¥0.00'" class="mini-tag">
挂号费 {{ reconciliationSummary.registrationPrice }}
</text>
</view>
<view v-if="reconciliationSummary.registerOrderNo" class="recon-sub">
挂号单号 {{ reconciliationSummary.registerOrderNo }}
</view>
<view class="hero-data">
<view class="data-box">
<text class="data-label">总销售额</text>
<text class="data-val text-primary">{{ reconciliationSummary.totalSales }}</text>
</view>
<view class="data-divider" />
<view class="data-box">
<text class="data-label">总供货额</text>
<text class="data-val">{{ reconciliationSummary.totalSupply }}</text>
</view>
</view>
<view class="dashboard-grid">
<view class="dashboard-island">
<view class="island-head">
<text class="island-title">草药</text>
<text class="island-sales">销量 {{ reconciliationSummary.herbal.sales }}</text>
</view>
<view class="island-line"><text>销售</text><text>{{ reconciliationSummary.herbal.salesPrice }}</text></view>
<view class="island-line"><text>供货</text><text>{{ reconciliationSummary.herbal.supplyPrice }}</text></view>
</view>
<view class="dashboard-island">
<view class="island-head">
<text class="island-title">西药</text>
<text class="island-sales">销量 {{ reconciliationSummary.medicine.sales }}</text>
</view>
<view class="island-line"><text>销售</text><text>{{ reconciliationSummary.medicine.salesPrice }}</text></view>
<view class="island-line"><text>供货</text><text>{{ reconciliationSummary.medicine.supplyPrice }}</text></view>
</view>
<view class="dashboard-island">
<view class="island-head">
<text class="island-title">服务包</text>
<text class="island-sales">销量 {{ reconciliationSummary.servicePackage.sales }}</text>
</view>
<view class="island-line"><text>销售</text><text>{{ reconciliationSummary.servicePackage.salesPrice }}</text></view>
<view class="island-line"><text>供货</text><text>{{ reconciliationSummary.servicePackage.supplyPrice }}</text></view>
</view>
<view class="dashboard-island">
<view class="island-head">
<text class="island-title">其他费用</text>
</view>
<view class="island-line"><text>诊疗费</text><text>{{ reconciliationSummary.otherFees.treatment }}</text></view>
<view class="island-line"><text>加工费</text><text>{{ reconciliationSummary.otherFees.process }}</text></view>
<view class="island-line"><text>快递费</text><text>{{ reconciliationSummary.otherFees.express }}</text></view>
</view>
</view>
</view>
</template>
<view v-if="reconciliationItems.length" class="recon-drug-section">
<view class="section-title">药品明细</view>
<view v-for="row in reconciliationItems" :key="row._wxKey" class="detail-card">
<view class="row"><text class="label">药品</text><text class="value">{{ row.drug_name || row.drug_id || '-' }}</text></view>
<view class="row"><text class="label">数量</text><text class="value">{{ row.number }}</text></view>
<view class="row"><text class="label">销售价</text><text class="value">{{ formatMoney(row.total_price) }}</text></view>
<view class="row"><text class="label">供货价</text><text class="value">{{ formatMoney(row.total_buy_price) }}</text></view>
</view>
</view>
<view v-else-if="!reconciliationHint && !reconciliationSummary" class="empty">暂无对账明细</view>
</template>
</view>
</template>
<view v-else-if="!loading" class="empty">加载失败请返回重试</view>
</view>
</clinic-page-layout>
</template>
<script>
import ClinicPageLayout from '../../components/clinic-page-layout.vue'
import { formatMoney } from '../../common/format.js'
import { mapReconciliationOrderSummary } from '../../common/display.js'
import {
getOrderTrace,
getOrderLedgerDetail,
getOrderReconciliationDetail,
} from '@/api/clinicAdmin.js'
import { withWxKey } from '@/utils/wxListKey.js'
export default {
components: { ClinicPageLayout },
data() {
return {
orderId: 0,
scene: 'product',
loading: true,
traceData: null,
activeTab: 'timeline',
ledgerSubTab: 'product',
ledgerProduct: null,
ledgerRegister: null,
ledgerLoading: false,
reconciliationData: null,
reconciliationLoading: false,
loadedTabs: {},
mainTabs: [
{ key: 'timeline', label: '时间线' },
{ key: 'ledger', label: '分账' },
{ key: 'reconciliation', label: '对账' },
],
}
},
computed: {
summary() {
return this.traceData?.summary || {}
},
timeline() {
return this.traceData?.timeline || []
},
links() {
return this.traceData?.links || {}
},
hasRegisterLedger() {
return !!(this.links.register_id && this.ledgerRegister !== null)
},
currentLedgerItems() {
const data = this.ledgerSubTab === 'register' ? this.ledgerRegister : this.ledgerProduct
const items = data?.items || []
return withWxKey(items, 'id', 'traceLed')
},
reconciliationItems() {
const items = this.reconciliationData?.items || []
return withWxKey(items, 'id', 'traceRec')
},
reconciliationSummary() {
return mapReconciliationOrderSummary(this.reconciliationData?.summary)
},
reconciliationHint() {
return this.reconciliationData?.reconciliation_hint || this.summary.reconciliation_hint || ''
},
},
onLoad(q) {
this.orderId = Number(q.order_id || 0)
this.scene = q.scene || 'product'
this.loadTrace()
},
methods: {
formatMoney,
loadTrace() {
if (this.orderId <= 0) {
this.loading = false
return
}
this.loading = true
getOrderTrace({ scene: this.scene, order_id: this.orderId }).then(w => {
if (w.ok) {
this.traceData = w.data
} else {
uni.showToast({ title: (w.res && w.res.message) || '加载失败', icon: 'none' })
}
}).finally(() => { this.loading = false })
},
switchMainTab(key) {
this.activeTab = key
if (key === 'ledger' && !this.loadedTabs.ledger) {
this.loadLedger()
} else if (key === 'reconciliation' && !this.loadedTabs.reconciliation) {
this.loadReconciliation()
}
},
switchLedgerSub(key) {
this.ledgerSubTab = key
},
loadLedger() {
const productId = this.links.product_order_id
const registerId = this.links.register_id
if (!productId && !registerId) {
this.loadedTabs.ledger = true
return
}
this.ledgerLoading = true
const reqs = []
if (productId) {
reqs.push(
getOrderLedgerDetail({ order_id: productId, order_type: 1, scope: 'store' }).then(w => {
this.ledgerProduct = w.ok ? w.data : null
})
)
} else {
this.ledgerProduct = null
}
if (registerId) {
reqs.push(
getOrderLedgerDetail({ order_id: registerId, order_type: 2, scope: 'store' }).then(w => {
this.ledgerRegister = w.ok ? w.data : null
})
)
} else {
this.ledgerRegister = null
}
Promise.all(reqs).finally(() => {
this.ledgerLoading = false
this.loadedTabs.ledger = true
})
},
loadReconciliation() {
const productId = this.links.product_order_id || (this.scene === 'product' ? this.orderId : 0)
if (!productId) {
this.reconciliationData = { items: [], reconciliation_hint: '暂无关联商品订单' }
this.loadedTabs.reconciliation = true
return
}
this.reconciliationLoading = true
getOrderReconciliationDetail({ product_order_id: productId }).then(w => {
if (w.ok) {
this.reconciliationData = w.data
}
}).finally(() => {
this.reconciliationLoading = false
this.loadedTabs.reconciliation = true
})
},
},
}
</script>
<style lang="scss" scoped>
.trace-container {
padding: 24rpx;
padding-bottom: 48rpx;
}
.loading-tip, .empty {
text-align: center;
color: #999;
font-size: 28rpx;
padding: 48rpx 0;
}
.summary-card, .detail-card {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 24rpx;
}
.summary-card .row, .detail-card .row {
display: flex;
justify-content: space-between;
font-size: 26rpx;
padding: 8rpx 0;
color: #333;
}
.label { color: #888; flex-shrink: 0; margin-right: 16rpx; }
.value { text-align: right; word-break: break-all; }
.amount { color: #1677ff; font-weight: 600; }
.main-tabs, .sub-tabs {
display: flex;
background: #fff;
border-radius: 12rpx;
margin-bottom: 24rpx;
overflow: hidden;
}
.main-tab, .sub-tab {
flex: 1;
text-align: center;
padding: 20rpx 0;
font-size: 28rpx;
color: #666;
}
.main-tab.active, .sub-tab.active {
color: #1677ff;
font-weight: 600;
background: #e6f4ff;
}
.tab-panel { min-height: 200rpx; }
.timeline-item {
display: flex;
margin-bottom: 24rpx;
padding-left: 8rpx;
}
.tl-dot {
width: 16rpx;
height: 16rpx;
border-radius: 50%;
background: #1677ff;
margin-top: 10rpx;
margin-right: 20rpx;
flex-shrink: 0;
}
.tl-body { flex: 1; background: #fff; border-radius: 12rpx; padding: 20rpx; }
.tl-title { font-size: 28rpx; font-weight: 600; color: #333; }
.tl-meta { font-size: 24rpx; color: #999; margin-top: 8rpx; }
.tl-status { margin-left: 12rpx; color: #1677ff; }
.tl-extra { font-size: 24rpx; color: #666; margin-top: 8rpx; }
.hint-box {
background: #fff7e6;
color: #d48806;
font-size: 26rpx;
padding: 20rpx 24rpx;
border-radius: 12rpx;
margin-bottom: 24rpx;
}
.sum-row {
display: flex;
justify-content: space-between;
font-size: 26rpx;
color: #333;
margin-bottom: 16rpx;
padding: 0 8rpx;
}
.recon-summary-card {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 24rpx;
}
.recon-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12rpx;
}
.recon-title {
font-size: 30rpx;
font-weight: 600;
color: #333;
}
.recon-sub {
font-size: 24rpx;
color: #888;
margin-bottom: 16rpx;
}
.mini-tag {
font-size: 20rpx;
padding: 4rpx 12rpx;
background: rgba(106, 205, 187, 0.1);
color: #6ACDBB;
border-radius: 8rpx;
}
.hero-data {
display: flex;
align-items: center;
background: #F8FBFB;
border-radius: 12rpx;
padding: 24rpx 0;
margin-bottom: 20rpx;
}
.data-box {
flex: 1;
text-align: center;
}
.data-label {
display: block;
font-size: 24rpx;
color: #888;
margin-bottom: 8rpx;
}
.data-val {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.text-primary { color: #1677ff; }
.data-divider {
width: 1rpx;
height: 60rpx;
background: #eee;
}
.dashboard-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16rpx;
}
.dashboard-island {
background: #F8FBFB;
border-radius: 12rpx;
padding: 16rpx;
}
.island-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12rpx;
}
.island-title {
font-size: 26rpx;
font-weight: 600;
color: #333;
}
.island-sales {
font-size: 22rpx;
color: #888;
}
.island-line {
display: flex;
justify-content: space-between;
font-size: 24rpx;
color: #666;
padding: 4rpx 0;
}
.recon-drug-section { margin-top: 8rpx; }
.section-title {
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 16rpx;
padding: 0 8rpx;
}
</style>

View File

@@ -0,0 +1,215 @@
<template>
<clinic-page-layout title="资金变动" :show-back="true">
<view class="page-container">
<view class="filter-bar">
<picker :range="sourceTypeLabels" @change="onSourceTypeChange">
<view class="picker-value">{{ currentSourceLabel }}</view>
</picker>
<input
v-model="orderNo"
class="search-input"
placeholder="订单号"
confirm-type="search"
@confirm="reload"
/>
<view class="search-btn" @click="reload">搜索</view>
</view>
<view class="list-wrap">
<view v-for="item in list" :key="item.id" class="record-card">
<view class="row head">
<text class="source">{{ item.source_type_txt || '-' }}</text>
<text class="change" :class="Number(item.change_amount) >= 0 ? 'inc' : 'dec'">
{{ item.change_amount }}
</text>
</view>
<view class="row"><text class="label">变更字段</text><text class="value">{{ item.field_name_txt || item.field_name }}</text></view>
<view class="row"><text class="label">变动前</text><text class="value">{{ item.before_amount }}</text></view>
<view class="row"><text class="label">变动后</text><text class="value">{{ item.after_amount }}</text></view>
<view class="row" v-if="item.order_no"><text class="label">订单号</text><text class="value">{{ item.order_no }}</text></view>
<view class="row"><text class="label">时间</text><text class="value">{{ item.created_at }}</text></view>
<view class="row" v-if="item.remark"><text class="label">备注</text><text class="value">{{ item.remark }}</text></view>
<view
v-if="Number(item.order_type) === 1 && item.order_id"
class="order-link"
@click="goOrderDetail(item)"
>查看订单详情</view>
</view>
</view>
<view v-if="!loading && list.length === 0" class="empty-state">暂无资金变动记录</view>
</view>
</clinic-page-layout>
</template>
<script>
import ClinicPageLayout from '../../components/clinic-page-layout.vue'
import { getAccountChangeLogList } from '@/api/clinicAdmin.js'
export default {
components: { ClinicPageLayout },
data() {
return {
list: [],
page: 1,
loading: false,
orderNo: '',
sourceType: '',
sourceTypeOptions: [
{ label: '全部来源', value: '' },
{ label: '分账', value: 'settle' },
{ label: '提现', value: 'withdraw' },
{ label: '退款', value: 'refund' },
],
}
},
computed: {
sourceTypeLabels() {
return this.sourceTypeOptions.map(o => o.label)
},
currentSourceLabel() {
const hit = this.sourceTypeOptions.find(o => o.value === this.sourceType)
return hit ? hit.label : '全部来源'
},
},
onShow() {
this.reload()
},
onReachBottom() {
this.load(this.page + 1, true)
},
methods: {
onSourceTypeChange(e) {
const idx = Number(e.detail.value || 0)
this.sourceType = this.sourceTypeOptions[idx].value
this.reload()
},
reload() {
this.load(1, false)
},
load(page, append) {
this.loading = true
const params = { page, pageSize: 15 }
if (this.sourceType) params.source_type = this.sourceType
if (this.orderNo.trim()) params.order_no = this.orderNo.trim()
getAccountChangeLogList(params).then(w => {
const items = (w.data && w.data.items) || []
this.list = append ? this.list.concat(items) : items
this.page = page
}).finally(() => { this.loading = false })
},
goOrderDetail(item) {
uni.navigateTo({
url: '/subPackages/sub_clinic_admin/order/detail?id=' + item.order_id,
})
},
},
}
</script>
<style lang="scss" scoped>
$color-bg-base: #F5F7F8;
$color-text-main: #1D2129;
$color-text-muted: #86909C;
$theme-primary: #6acdbb;
.page-container {
min-height: 100vh;
background: $color-bg-base;
padding: 24rpx;
}
.filter-bar {
display: flex;
gap: 12rpx;
margin-bottom: 24rpx;
align-items: center;
}
.picker-value,
.search-input {
background: #fff;
border-radius: 12rpx;
padding: 16rpx 20rpx;
font-size: 26rpx;
}
.picker-value {
min-width: 160rpx;
}
.search-input {
flex: 1;
}
.search-btn {
background: $theme-primary;
color: #fff;
border-radius: 12rpx;
padding: 16rpx 24rpx;
font-size: 26rpx;
}
.list-wrap {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.record-card {
background: #fff;
border-radius: 20rpx;
padding: 28rpx;
}
.row {
display: flex;
justify-content: space-between;
font-size: 26rpx;
padding: 8rpx 0;
}
.row.head {
border-bottom: 1rpx dashed #E5E6EB;
margin-bottom: 12rpx;
padding-bottom: 16rpx;
}
.source {
font-weight: 600;
color: $color-text-main;
}
.change {
font-weight: 700;
font-family: DIN Alternate, monospace, sans-serif;
}
.inc { color: $theme-primary; }
.dec { color: #F53F3F; }
.label {
color: $color-text-muted;
}
.value {
color: $color-text-main;
text-align: right;
flex: 1;
margin-left: 16rpx;
word-break: break-all;
}
.order-link {
margin-top: 16rpx;
text-align: right;
color: $theme-primary;
font-size: 26rpx;
}
.empty-state {
text-align: center;
color: $color-text-muted;
padding: 100rpx 0;
}
</style>

View File

@@ -91,12 +91,13 @@ export default {
},
balanceItems: [
{ key: 'balance', label: '可提现余额 (元)', display: '0.00' },
{ key: 'frozen', label: '冻结金额 (元)', display: '0.00' }
{ key: 'reviewing', label: '审核中金额 (元)', display: '0.00' },
{ key: 'pending', label: '待结算收益 (元)', display: '0.00' },
],
actions: [
{ key: 'apply', label: '申请提现' },
// { key: 'edit', label: '修改账户' },
{ key: 'settlement', label: '结算记录' }
{ key: 'settlement', label: '结算记录' },
{ key: 'accountChange', label: '资金变动' },
],
withdrawalList: [],
page: 1,
@@ -129,6 +130,8 @@ export default {
uni.navigateTo({ url: '/subPackages/sub_clinic_admin/withdrawal/card-edit' });
} else if (key === 'settlement') {
uni.navigateTo({ url: '/subPackages/sub_clinic_admin/withdrawal/settlement/index' });
} else if (key === 'accountChange') {
uni.navigateTo({ url: '/subPackages/sub_clinic_admin/withdrawal/account-change/index' });
}
},
@@ -151,9 +154,16 @@ export default {
loadBalance() {
getClinicAdminBalance().then(w => {
if (w.ok && w.data) {
// 格式化金额并去除 ¥ 符号以便大字号展示
this.balanceItems[0].display = formatMoney(w.data.balance || 0).replace('¥', '');
this.balanceItems[1].display = formatMoney(w.data.frozen_balance || 0).replace('¥', '');
const fmt = (v) => formatMoney(v || 0).replace('¥', '')
const map = {
balance: fmt(w.data.balance),
reviewing: fmt(w.data.withdrawn_frozen),
pending: fmt(w.data.pending_earnings),
}
this.balanceItems = this.balanceItems.map(item => ({
...item,
display: map[item.key] ?? '0.00',
}))
}
});
},
@@ -265,7 +275,7 @@ $color-text-muted: #86909C;
/* 统计网格 */
.stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-columns: repeat(3, 1fr);
gap: 20rpx;
margin-bottom: 24rpx;
}

View File

@@ -47,6 +47,8 @@ export default {
data() {
return {
ledgerLogId: 0,
orderId: 0,
orderType: 0,
detail: { items: [], sum_money: '0' },
ledgerLogSummary: null,
ledgerItems: [],
@@ -55,19 +57,22 @@ export default {
},
onLoad(q) {
this.ledgerLogId = Number(q.ledger_log_id || 0)
this.orderId = Number(q.order_id || 0)
this.orderType = Number(q.order_type || 0)
this.load()
},
methods: {
formatMoney,
load() {
this.loading = true
getLedgerDetail({ ledger_log_id: this.ledgerLogId }).then(w => {
const params = this.orderId > 0 && this.orderType > 0
? { order_id: this.orderId, order_type: this.orderType }
: { ledger_log_id: this.ledgerLogId }
getLedgerDetail(params).then(w => {
if (w.ok) {
this.detail = w.data || {}
const log = this.detail.ledger_log
if (log) {
this.ledgerLogSummary = log
}
this.ledgerLogSummary = log || null
this.ledgerItems = withWxKey(this.detail.items || [], 'id', 'led')
}
}).finally(() => { this.loading = false })

View File

@@ -1,14 +1,27 @@
<template>
<clinic-page-layout title="结算记录" :show-back="true">
<view class="settlement-container">
<view class="filter-bar">
<input
v-model="orderNo"
class="search-input"
placeholder="搜索订单号"
confirm-type="search"
@confirm="reload"
/>
<view class="search-btn" @click="reload">搜索</view>
</view>
<view class="merge-bar">
<text class="merge-label">同订单合并</text>
<switch :checked="mergeByOrder" color="#6acdbb" @change="onMergeChange" />
</view>
<view class="list-wrap">
<view
v-for="item in list"
:key="item.id"
class="premium-card"
hover-class="card-hover"
@click="goDetail(item)"
>
<view class="card-head">
<view class="order-no">
@@ -21,6 +34,10 @@
</view>
<view class="card-body">
<view class="info-row" v-if="rowDisplay(item).orderTypeText">
<text class="label">订单类型</text>
<text class="value">{{ rowDisplay(item).orderTypeText }}</text>
</view>
<view class="info-row">
<text class="label">费用类型</text>
<text class="value">{{ rowDisplay(item).feeTypeText }}</text>
@@ -34,6 +51,15 @@
<text class="value">{{ rowDisplay(item).content }}</text>
</view>
</view>
<view class="card-actions">
<text class="action-link" @click.stop="goSettlementDetail(item)">结算明细</text>
<text
v-if="Number(item.order_type) === 1"
class="action-link primary"
@click.stop="goOrderDetail(item)"
>订单详情</text>
</view>
</view>
</view>
@@ -56,6 +82,8 @@ export default {
list: [],
page: 1,
loading: false,
mergeByOrder: true,
orderNo: '',
}
},
onShow() {
@@ -68,22 +96,51 @@ export default {
rowDisplay(item) {
return mapSettlementRow(item)
},
onMergeChange(e) {
this.mergeByOrder = !!e.detail.value
this.reload()
},
reload() {
this.load(1, false)
},
load(page, append) {
this.loading = true
getLedgerLogList({ page, pageSize: 15 }).then(w => {
const params = {
page,
pageSize: 15,
merge_by_order: this.mergeByOrder ? 1 : 0,
exclude_zero_amount: 1,
}
if (this.orderNo.trim()) {
params.order_no = this.orderNo.trim()
}
getLedgerLogList(params).then(w => {
const items = (w.data && w.data.items) || []
this.list = append ? this.list.concat(items) : items
this.page = page
}).finally(() => { this.loading = false })
},
goDetail(item) {
goSettlementDetail(item) {
if (item.merged) {
uni.navigateTo({
url: '/subPackages/sub_clinic_admin/withdrawal/settlement/detail?order_id='
+ item.order_id + '&order_type=' + item.order_type,
})
return
}
uni.navigateTo({
url: '/subPackages/sub_clinic_admin/withdrawal/settlement/detail?ledger_log_id=' + item.id,
})
},
goOrderDetail(item) {
if (Number(item.order_type) !== 1 || !item.order_id) {
uni.showToast({ title: '暂不支持查看该订单详情', icon: 'none' })
return
}
uni.navigateTo({
url: '/subPackages/sub_clinic_admin/order/detail?id=' + item.order_id,
})
},
},
}
</script>
@@ -93,6 +150,7 @@ $color-bg-base: #F5F7F8;
$color-text-main: #1D2129;
$color-text-body: #4E5969;
$color-text-muted: #86909C;
$theme-primary: #6acdbb;
.settlement-container {
background-color: $color-bg-base;
@@ -100,6 +158,45 @@ $color-text-muted: #86909C;
padding: 24rpx;
}
.filter-bar {
display: flex;
gap: 16rpx;
margin-bottom: 16rpx;
}
.search-input {
flex: 1;
background: #fff;
border-radius: 12rpx;
padding: 16rpx 24rpx;
font-size: 28rpx;
}
.search-btn {
background: $theme-primary;
color: #fff;
border-radius: 12rpx;
padding: 0 28rpx;
display: flex;
align-items: center;
font-size: 28rpx;
}
.merge-bar {
display: flex;
align-items: center;
justify-content: space-between;
background: #ffffff;
border-radius: 16rpx;
padding: 20rpx 28rpx;
margin-bottom: 24rpx;
}
.merge-label {
font-size: 28rpx;
color: $color-text-main;
}
.list-wrap {
display: flex;
flex-direction: column;
@@ -111,12 +208,6 @@ $color-text-muted: #86909C;
border-radius: 24rpx;
padding: 32rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.02);
transition: background-color 0.2s;
}
.card-hover {
background-color: #FAFAFA;
transform: scale(0.98);
}
.card-head {
@@ -155,10 +246,8 @@ $color-text-muted: #86909C;
font-family: DIN Alternate, monospace, sans-serif;
}
/* 兼容你原有的类名逻辑 */
.text-danger { color: #F53F3F !important; }
.text-success { color: #6acdbb !important; }
.text-warning { color: #F5A623 !important; }
.card-body {
display: flex;
@@ -185,10 +274,29 @@ $color-text-muted: #86909C;
word-break: break-all;
}
.card-actions {
display: flex;
justify-content: flex-end;
gap: 32rpx;
margin-top: 24rpx;
padding-top: 20rpx;
border-top: 1rpx solid #F2F3F5;
}
.action-link {
font-size: 26rpx;
color: $color-text-muted;
}
.action-link.primary {
color: $theme-primary;
font-weight: 600;
}
.empty-state {
text-align: center;
color: $color-text-muted;
padding: 120rpx 0;
font-size: 28rpx;
}
</style>
</style>