feat: 语音开方、修复分账问题、识图成方、识图传方、优化订单卡片展示
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
/**
|
||||
* 门店详情 - 资金账户面板(余额 + 资金流水 + 账户变动)
|
||||
* 仅超管/系统管理员在 StoreCardModal 中可见,数据按门店 ID 查询
|
||||
* 口径说明:资金流水按履约店 store_id;账户变动/余额按分账利润归属店 user_id
|
||||
*/
|
||||
import type { Dayjs } from 'dayjs';
|
||||
|
||||
@@ -65,6 +66,9 @@ const fundWaterColumns = [
|
||||
{ title: '金额', dataIndex: 'price', key: 'price', width: 110 },
|
||||
{ title: '流水类型', dataIndex: 'type', key: 'type', width: 110 },
|
||||
{ title: '订单类型', dataIndex: 'order_type', key: 'order_type', width: 110 },
|
||||
// store_id 便于核对是否本店履约(委托复诊时流水店与利润店可能不同)
|
||||
{ title: '门店ID', dataIndex: 'store_id', key: 'store_id', width: 90 },
|
||||
{ title: '诊所', dataIndex: 'store_name', key: 'store_name', width: 120, ellipsis: true },
|
||||
{ title: '订单号', dataIndex: 'order_no', key: 'order_no', ellipsis: true },
|
||||
{ title: '医生', dataIndex: 'doctor_name', key: 'doctor_name', width: 100 },
|
||||
{ title: '时间', dataIndex: 'created_at', key: 'created_at', width: 170 },
|
||||
@@ -77,6 +81,7 @@ const changeLogColumns = [
|
||||
{ title: '变动前', dataIndex: 'before_amount', key: 'before_amount', width: 100 },
|
||||
{ title: '变动后', dataIndex: 'after_amount', key: 'after_amount', width: 100 },
|
||||
{ title: '变动值', dataIndex: 'change_amount', key: 'change_amount', width: 100 },
|
||||
{ title: '主体ID', dataIndex: 'user_id', key: 'user_id', width: 90 },
|
||||
{ title: '订单号', dataIndex: 'order_no', key: 'order_no', ellipsis: true },
|
||||
{ title: '来源表', dataIndex: 'source_table_txt', key: 'source_table_txt', width: 110 },
|
||||
{ title: '备注', dataIndex: 'remark', key: 'remark', ellipsis: true },
|
||||
@@ -123,6 +128,7 @@ async function loadFundWater(page = fundWaterPage.value) {
|
||||
fundWaterList.value = (res?.items || []).map((item: any) => ({
|
||||
...item,
|
||||
doctor_name: item?.doctor_info?.name || '-',
|
||||
store_name: item?.store?.name || '-',
|
||||
}));
|
||||
fundWaterTotal.value = Number(res?.total || 0);
|
||||
} catch (e: any) {
|
||||
@@ -237,6 +243,9 @@ function orderTypeTag(orderType: number) {
|
||||
</section>
|
||||
|
||||
<section class="finance-section finance-section--list">
|
||||
<p class="finance-hint">
|
||||
资金流水按订单履约店;账户变动/余额按分账利润归属店。委托复诊等场景两边主体可能不同,属业务口径而非串店。
|
||||
</p>
|
||||
<Segmented
|
||||
v-model:value="activeListTab"
|
||||
block
|
||||
@@ -266,7 +275,7 @@ function orderTypeTag(orderType: number) {
|
||||
}"
|
||||
row-key="id"
|
||||
size="small"
|
||||
:scroll="{ x: 800, y: 320 }"
|
||||
:scroll="{ x: 1000, y: 320 }"
|
||||
@change="(pag: any) => {
|
||||
fundWaterPageSize = pag.pageSize || fundWaterPageSize;
|
||||
loadFundWater(pag.current || 1);
|
||||
@@ -311,12 +320,24 @@ function orderTypeTag(orderType: number) {
|
||||
}"
|
||||
row-key="id"
|
||||
size="small"
|
||||
:scroll="{ x: 960, y: 320 }"
|
||||
:scroll="{ x: 1080, y: 320 }"
|
||||
@change="(pag: any) => {
|
||||
changeLogPageSize = pag.pageSize || changeLogPageSize;
|
||||
loadChangeLog(pag.current || 1);
|
||||
}"
|
||||
/>
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template
|
||||
v-if="
|
||||
column.key === 'before_amount' ||
|
||||
column.key === 'after_amount' ||
|
||||
column.key === 'change_amount'
|
||||
"
|
||||
>
|
||||
{{ formatMoney(record[column.key]) }}
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -354,6 +375,13 @@ function orderTypeTag(orderType: number) {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.finance-hint {
|
||||
margin: 0 0 8px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.finance-list-segment {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ export const ENCRYPT_FIELDS = new Set([
|
||||
'phone',
|
||||
'open_api_key',
|
||||
'api_key',
|
||||
'app_secret',
|
||||
]);
|
||||
|
||||
/** 需要生成 _tm_text 并默认脱敏展示的字段(encrypt_only) */
|
||||
@@ -98,7 +99,12 @@ export function maskSensitiveField(field: string, plainText: unknown): string {
|
||||
return '';
|
||||
}
|
||||
const str = String(plainText);
|
||||
if (field === 'password' || field === 'api_key' || field === 'open_api_key') {
|
||||
if (
|
||||
field === 'password' ||
|
||||
field === 'api_key' ||
|
||||
field === 'open_api_key' ||
|
||||
field === 'app_secret'
|
||||
) {
|
||||
return '*'.repeat(Math.max(str.length, 6));
|
||||
}
|
||||
if (PHONE_FIELDS.has(field)) {
|
||||
|
||||
@@ -1,5 +1,62 @@
|
||||
export type ProcessRuleListItem = { id?: number | string };
|
||||
|
||||
/** 代煎+用量必填校验入参(对齐小程序 validateChineseConfigRequired) */
|
||||
export type ChineseConfigRequiredInput = {
|
||||
ruleType?: number | null;
|
||||
packageMethodId?: number | null;
|
||||
processRuleId?: number | null;
|
||||
childProcessRuleId?: number | null;
|
||||
processRuleNoteId?: number | null;
|
||||
dosage?: number | null;
|
||||
dayDosage?: number | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* 中药制剂方式 / 代煎 / 用量必填校验
|
||||
* 返回 keys 供 ChineseMedicineConfig 标红,message 供 toast
|
||||
*/
|
||||
export function validateChineseConfigRequired(config: ChineseConfigRequiredInput) {
|
||||
const keys: string[] = [];
|
||||
const messages: string[] = [];
|
||||
const ruleType = Number(config?.ruleType || 0);
|
||||
if (ruleType !== 1 && ruleType !== 2) {
|
||||
keys.push('ruleType');
|
||||
messages.push('请选择制剂方式');
|
||||
}
|
||||
if (ruleType === 1) {
|
||||
if (!Number(config?.packageMethodId)) {
|
||||
keys.push('packageMethodId');
|
||||
messages.push('请选择包法');
|
||||
}
|
||||
} else if (ruleType === 2) {
|
||||
if (!Number(config?.processRuleId)) {
|
||||
keys.push('processRuleId');
|
||||
messages.push('请选择制剂要求');
|
||||
}
|
||||
if (!Number(config?.childProcessRuleId)) {
|
||||
keys.push('childProcessRuleId');
|
||||
messages.push('请选择煎法说明');
|
||||
}
|
||||
if (!Number(config?.processRuleNoteId)) {
|
||||
keys.push('processRuleNoteId');
|
||||
messages.push('请选择附加备注');
|
||||
}
|
||||
}
|
||||
if (!(Number(config?.dosage) >= 1)) {
|
||||
keys.push('dosage');
|
||||
messages.push('请填写剂数');
|
||||
}
|
||||
if (!(Number(config?.dayDosage) >= 1)) {
|
||||
keys.push('dayDosage');
|
||||
messages.push('请填写用药频次');
|
||||
}
|
||||
return {
|
||||
valid: keys.length === 0,
|
||||
keys,
|
||||
message: messages[0] || '',
|
||||
};
|
||||
}
|
||||
|
||||
export type ProcessRuleLoadedLevel = 'root' | 'child' | 'note';
|
||||
|
||||
/**
|
||||
|
||||
@@ -78,6 +78,7 @@ import { ChineseDrugNameBubble } from '#/components/chinese-drug-name-bubble';
|
||||
import { formatPriceDisplay } from '#/utils/formatPrice';
|
||||
import ChineseMedicineConfig from '#/views/doctor/doctor-reception/components/ChineseMedicineConfig.vue';
|
||||
import { getDeliveryWarehouseOptionsByDrugs, getPrescriptionInfoApi } from '#/views/doctor/doctor-reception/api';
|
||||
import { alignChineseDrugsToLatestPrice } from '#/views/doctor/doctor-reception/utils/alignChineseDrugPrice';
|
||||
import WarehouseSelectModal from './WarehouseSelectModal.vue';
|
||||
import { detailDoctorOrderToText } from '#/utils/prescriptionDoctorOrder';
|
||||
|
||||
@@ -1008,20 +1009,25 @@ async function reusePrescriptionFromList(prescriptionId: number) {
|
||||
message.error('解析中药处方失败');
|
||||
return;
|
||||
}
|
||||
prescriptionStore.updateCurrentDrugs(
|
||||
(rows || []).map((d: any) => {
|
||||
const drugId = Number(d.drug_id ?? d.id);
|
||||
return {
|
||||
index_id: d.id ?? drugId,
|
||||
id: drugId,
|
||||
drug_name: d.name || d.drug_name || '',
|
||||
number: Number(d.number ?? d.amount ?? 0),
|
||||
way_id: d.way_id ?? d.use_way?.id ?? d.order,
|
||||
price: Number(d.price ?? d.sell_price ?? 0),
|
||||
unit: d.unit,
|
||||
};
|
||||
}),
|
||||
);
|
||||
const mappedChinese = (rows || []).map((d: any) => {
|
||||
const drugId = Number(d.drug_id ?? d.id);
|
||||
return {
|
||||
index_id: d.id ?? drugId,
|
||||
id: drugId,
|
||||
drug_name: d.name || d.drug_name || '',
|
||||
number: Number(d.number ?? d.amount ?? 0),
|
||||
way_id: d.way_id ?? d.use_way?.id ?? d.order,
|
||||
price: Number(d.price ?? d.sell_price ?? 0),
|
||||
buy_price: d.buy_price ?? null,
|
||||
unit: d.unit,
|
||||
};
|
||||
});
|
||||
await alignChineseDrugsToLatestPrice({
|
||||
registerId: medicalRecordRegisterId.value,
|
||||
storeId: medicalRecordStoreId.value,
|
||||
drugs: mappedChinese,
|
||||
});
|
||||
prescriptionStore.updateCurrentDrugs(mappedChinese);
|
||||
if (rep0?.dosage != null) prescriptionStore.dosage = Number(rep0.dosage) || prescriptionStore.dosage;
|
||||
if (rep0?.day_dosage != null) {
|
||||
prescriptionStore.dayDosage = Number(rep0.day_dosage) || prescriptionStore.dayDosage;
|
||||
@@ -1297,19 +1303,26 @@ async function handleSelectCommonPrescription(data: any, type: number) {
|
||||
});
|
||||
} else {
|
||||
// 中药/颗粒药处方
|
||||
recipes.forEach((recipe: any) => {
|
||||
const newProduct = {
|
||||
index_id: recipe.drug_id || recipe.id,
|
||||
id: recipe.drug_id || recipe.id,
|
||||
drug_name: recipe.drug_name || recipe.name,
|
||||
number: recipe.number || 1,
|
||||
price: recipe.price || 0,
|
||||
buy_price: recipe.buy_price,
|
||||
way_id: recipe.way_id || 0,
|
||||
specification: recipe.specification || recipe.drug?.specification || '',
|
||||
select_number: 1,
|
||||
};
|
||||
// 检查是否已存在
|
||||
const chineseRows = recipes.map((recipe: any) => ({
|
||||
index_id: recipe.drug_id || recipe.id,
|
||||
id: recipe.drug_id || recipe.id,
|
||||
drug_name: recipe.drug_name || recipe.name,
|
||||
number: recipe.number || 1,
|
||||
price: recipe.price || 0,
|
||||
buy_price: recipe.buy_price,
|
||||
way_id: recipe.way_id || 0,
|
||||
specification: recipe.specification || recipe.drug?.specification || '',
|
||||
select_number: 1,
|
||||
}));
|
||||
// 颗粒药不走供货比例,只核对中药常用方
|
||||
if (type === 2) {
|
||||
await alignChineseDrugsToLatestPrice({
|
||||
registerId: medicalRecordRegisterId.value,
|
||||
storeId: medicalRecordStoreId.value,
|
||||
drugs: chineseRows,
|
||||
});
|
||||
}
|
||||
chineseRows.forEach((newProduct) => {
|
||||
const existItem = prescriptionStore.currentDrugs.find(
|
||||
(item: any) => item.id === newProduct.id,
|
||||
);
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
Table,
|
||||
Tabs,
|
||||
Tag,
|
||||
Timeline,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
@@ -45,6 +44,10 @@ const reconciliationLoading = ref(false);
|
||||
const activeTab = ref('timeline');
|
||||
const ledgerScope = ref<LedgerScope>('all');
|
||||
const ledgerSubTab = ref<'product' | 'register'>('product');
|
||||
/** 商品订单分账:真实入账行 / 下单时冻结的快照 */
|
||||
const productLedgerView = ref<'actual' | 'snapshot'>('actual');
|
||||
/** 时间线一行放几个节点,奇数行从右往左折返 */
|
||||
const TIMELINE_PER_ROW = 4;
|
||||
const traceData = ref<Record<string, any> | null>(null);
|
||||
const ledgerProduct = ref<Record<string, any> | null>(null);
|
||||
const ledgerRegister = ref<Record<string, any> | null>(null);
|
||||
@@ -73,6 +76,61 @@ const currentLedgerData = computed(() =>
|
||||
ledgerSubTab.value === 'product' ? ledgerProduct.value : ledgerRegister.value,
|
||||
);
|
||||
|
||||
/** 商品订单分账快照(无快照时为 null) */
|
||||
const productLedgerSnapshot = computed(
|
||||
() => ledgerProduct.value?.ledger_split ?? null,
|
||||
);
|
||||
|
||||
/**
|
||||
* 节点状态色:完成类用主题色,驳回/退款用警告色,纯记录用弱色。
|
||||
* 只根据已有文案归类,不改时间线数据。
|
||||
*/
|
||||
function timelineStatusTone(status: string) {
|
||||
if (/驳回|拒|退|失败|取消/.test(status)) {
|
||||
return 'warn';
|
||||
}
|
||||
if (!status || status === '记录') {
|
||||
return 'muted';
|
||||
}
|
||||
return 'ok';
|
||||
}
|
||||
|
||||
/**
|
||||
* 把时间线拆成固定四列的折返行。
|
||||
* 偶数行从左到右占第 1~4 列;奇数行从右往左,第 1 个节点落在最右列,和上一行末尾对齐。
|
||||
*/
|
||||
const timelineRows = computed(() => {
|
||||
const nodes = (traceData.value?.timeline ?? []).map((node: any, index: number) => ({
|
||||
...node,
|
||||
seq: index + 1,
|
||||
tone: timelineStatusTone(String(node.status ?? '')),
|
||||
}));
|
||||
const rows: Array<{
|
||||
nodes: Array<any & { column: number; pointLeft: boolean; hasNext: boolean }>;
|
||||
rtl: boolean;
|
||||
showTurn: boolean;
|
||||
turnColumn: number;
|
||||
}> = [];
|
||||
for (let i = 0; i < nodes.length; i += TIMELINE_PER_ROW) {
|
||||
const chunk = nodes.slice(i, i + TIMELINE_PER_ROW);
|
||||
const rowIndex = rows.length;
|
||||
const rtl = rowIndex % 2 === 1;
|
||||
const placed = chunk.map((node, index) => {
|
||||
const column = rtl ? TIMELINE_PER_ROW - index : index + 1;
|
||||
const hasNext = index < chunk.length - 1;
|
||||
return { ...node, column, hasNext, pointLeft: rtl };
|
||||
});
|
||||
const lastIndex = chunk.length - 1;
|
||||
rows.push({
|
||||
nodes: placed,
|
||||
rtl,
|
||||
showTurn: i + TIMELINE_PER_ROW < nodes.length,
|
||||
turnColumn: rtl ? TIMELINE_PER_ROW - lastIndex : lastIndex + 1,
|
||||
});
|
||||
}
|
||||
return rows;
|
||||
});
|
||||
|
||||
function formatDrugCell(value?: string) {
|
||||
return value?.trim() ? value : '-';
|
||||
}
|
||||
@@ -86,6 +144,7 @@ function resetDrawerState() {
|
||||
activeTab.value = 'timeline';
|
||||
ledgerScope.value = isPlatformAdmin.value ? 'all' : 'store';
|
||||
ledgerSubTab.value = 'product';
|
||||
productLedgerView.value = 'actual';
|
||||
}
|
||||
|
||||
const [Drawer, drawerApi] = useVbenDrawer({
|
||||
@@ -356,22 +415,44 @@ watch(isPlatformAdmin, (val) => {
|
||||
|
||||
<Tabs v-model:active-key="activeTab" @change="(key) => handleMainTabChange(String(key))">
|
||||
<Tabs.TabPane key="timeline" tab="溯源时间线">
|
||||
<Timeline v-if="traceData.timeline?.length">
|
||||
<Timeline.Item
|
||||
v-for="node in traceData.timeline"
|
||||
:key="node.key"
|
||||
<div v-if="timelineRows.length" class="trace-snake">
|
||||
<div
|
||||
v-for="(row, rowIndex) in timelineRows"
|
||||
:key="rowIndex"
|
||||
class="trace-snake-wrap"
|
||||
>
|
||||
<div class="font-medium">{{ node.title }}</div>
|
||||
<div class="text-sm text-gray-500">{{ node.time }}</div>
|
||||
<div v-if="node.status" class="text-sm">
|
||||
<Tag>{{ node.status }}</Tag>
|
||||
<div class="trace-snake-row">
|
||||
<div
|
||||
v-for="node in row.nodes"
|
||||
:key="node.key"
|
||||
class="trace-snake-node"
|
||||
:class="{
|
||||
'point-left': node.pointLeft && node.hasNext,
|
||||
'point-right': !node.pointLeft && node.hasNext,
|
||||
}"
|
||||
:style="{ gridColumn: String(node.column), gridRow: '1' }"
|
||||
>
|
||||
<div class="trace-snake-head">
|
||||
<span class="trace-snake-index">{{ node.seq }}</span>
|
||||
<span class="trace-snake-title">{{ node.title }}</span>
|
||||
</div>
|
||||
<span
|
||||
v-if="node.status"
|
||||
class="trace-snake-status"
|
||||
:class="'is-' + node.tone"
|
||||
>{{ node.status }}</span>
|
||||
<div v-if="node.time" class="trace-snake-time">{{ node.time }}</div>
|
||||
<div v-if="node.extra" class="trace-snake-extra" :title="node.extra">{{ node.extra }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="node.extra" class="mt-1 text-sm text-gray-600">
|
||||
{{ node.extra }}
|
||||
</div>
|
||||
</Timeline.Item>
|
||||
</Timeline>
|
||||
<div v-else class="py-6 text-center text-gray-500">暂无溯源记录</div>
|
||||
<div
|
||||
v-if="row.showTurn"
|
||||
class="trace-snake-turn"
|
||||
:style="{ left: `calc((${row.turnColumn} - 0.5) * 25%)` }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="py-6 text-center text-[hsl(var(--muted-foreground))]">暂无溯源记录</div>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane key="ledger" tab="分账明细">
|
||||
@@ -392,46 +473,76 @@ watch(isPlatformAdmin, (val) => {
|
||||
|
||||
<Tabs v-model:active-key="ledgerSubTab" type="card">
|
||||
<Tabs.TabPane key="product" tab="商品订单分账">
|
||||
<div class="mb-2 text-sm">
|
||||
合计分账:{{ ledgerProduct?.sum_money ?? '0' }}
|
||||
<span class="ml-2 text-xs text-[hsl(var(--muted-foreground))]">(不含已取消)</span>
|
||||
</div>
|
||||
<Table
|
||||
:columns="ledgerColumns"
|
||||
:data-source="ledgerProduct?.items ?? []"
|
||||
:pagination="false"
|
||||
row-key="id"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record, text }">
|
||||
<template v-if="column.key === 'drug_image'">
|
||||
<Image
|
||||
v-if="record.drug_image"
|
||||
:src="record.drug_image"
|
||||
:width="48"
|
||||
:height="48"
|
||||
class="object-cover"
|
||||
/>
|
||||
<span v-else>-</span>
|
||||
<Tabs v-model:active-key="productLedgerView" class="mb-3" size="small">
|
||||
<Tabs.TabPane key="actual" tab="真实分账" />
|
||||
<Tabs.TabPane key="snapshot" tab="快照分账" />
|
||||
</Tabs>
|
||||
<template v-if="productLedgerView === 'actual'">
|
||||
<div class="mb-2 text-sm">
|
||||
合计分账:{{ ledgerProduct?.sum_money ?? '0' }}
|
||||
<span class="ml-2 text-xs text-[hsl(var(--muted-foreground))]">(不含已取消)</span>
|
||||
</div>
|
||||
<Table
|
||||
:columns="ledgerColumns"
|
||||
:data-source="ledgerProduct?.items ?? []"
|
||||
:pagination="false"
|
||||
row-key="id"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record, text }">
|
||||
<template v-if="column.key === 'drug_image'">
|
||||
<Image
|
||||
v-if="record.drug_image"
|
||||
:src="record.drug_image"
|
||||
:width="48"
|
||||
:height="48"
|
||||
class="object-cover"
|
||||
/>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
<template
|
||||
v-else-if="
|
||||
column.key === 'drug_name' ||
|
||||
column.key === 'drug_number' ||
|
||||
column.key === 'specification' ||
|
||||
column.key === 'party_name'
|
||||
"
|
||||
>
|
||||
{{ formatDrugCell(text) }}
|
||||
</template>
|
||||
</template>
|
||||
<template
|
||||
v-else-if="
|
||||
column.key === 'drug_name' ||
|
||||
column.key === 'drug_number' ||
|
||||
column.key === 'specification' ||
|
||||
column.key === 'party_name'
|
||||
"
|
||||
>
|
||||
{{ formatDrugCell(text) }}
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
<div
|
||||
v-if="!ledgerProduct?.items?.length && !ledgerLoading"
|
||||
class="py-6 text-center text-gray-500"
|
||||
>
|
||||
暂无分账记录
|
||||
</div>
|
||||
</Table>
|
||||
<div
|
||||
v-if="!ledgerProduct?.items?.length && !ledgerLoading"
|
||||
class="py-6 text-center text-[hsl(var(--muted-foreground))]"
|
||||
>
|
||||
暂无分账记录
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<Descriptions
|
||||
v-if="productLedgerSnapshot"
|
||||
bordered
|
||||
size="small"
|
||||
:column="2"
|
||||
>
|
||||
<Descriptions.Item label="分账方式">
|
||||
{{ productLedgerSnapshot.split_type_txt || '-' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="平台占比">
|
||||
{{ productLedgerSnapshot.platform_percent }}%
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="平台金额">
|
||||
{{ productLedgerSnapshot.platform_amount }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="诊所金额">
|
||||
{{ productLedgerSnapshot.store_amount }}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<div v-else class="py-6 text-center text-[hsl(var(--muted-foreground))]">
|
||||
该订单还没有分账快照(上线前的旧单,或尚未结算)
|
||||
</div>
|
||||
</template>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane key="register" tab="挂号订单分账">
|
||||
<div class="mb-2 text-sm">
|
||||
@@ -497,3 +608,147 @@ watch(isPlatformAdmin, (val) => {
|
||||
<OrderDetailModal />
|
||||
</Drawer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 溯源时间线:固定四列网格,奇数行从右往左折返,列与上一行对齐 */
|
||||
.trace-snake {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 36px;
|
||||
padding: 8px 12px 16px;
|
||||
}
|
||||
.trace-snake-wrap {
|
||||
position: relative;
|
||||
}
|
||||
.trace-snake-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
grid-template-rows: auto;
|
||||
column-gap: 36px;
|
||||
align-items: stretch;
|
||||
}
|
||||
.trace-snake-node {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
min-height: 112px;
|
||||
padding: 12px;
|
||||
border: 1px solid hsl(var(--primary) / 30%);
|
||||
border-radius: 10px;
|
||||
background: hsl(var(--card, var(--background)));
|
||||
box-shadow: 0 0 6px hsl(var(--primary) / 18%);
|
||||
}
|
||||
.trace-snake-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 22px;
|
||||
}
|
||||
.trace-snake-index {
|
||||
display: inline-flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border: 1px solid hsl(var(--primary) / 30%);
|
||||
border-radius: 999px;
|
||||
background: hsl(var(--primary) / 12%);
|
||||
color: hsl(var(--primary));
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
}
|
||||
.trace-snake-title {
|
||||
min-width: 0;
|
||||
color: hsl(var(--foreground));
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
word-break: break-all;
|
||||
}
|
||||
.trace-snake-status {
|
||||
align-self: flex-start;
|
||||
max-width: 100%;
|
||||
padding: 0 8px;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 999px;
|
||||
background: hsl(var(--muted) / 35%);
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
}
|
||||
.trace-snake-status.is-ok {
|
||||
border-color: hsl(var(--primary) / 28%);
|
||||
background: hsl(var(--primary) / 12%);
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
.trace-snake-status.is-warn {
|
||||
border-color: hsl(var(--warning) / 35%);
|
||||
background: hsl(var(--warning) / 12%);
|
||||
color: hsl(var(--warning));
|
||||
}
|
||||
.trace-snake-time {
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-size: 12px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.trace-snake-extra {
|
||||
margin-top: auto;
|
||||
padding: 6px 8px;
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
background: hsl(var(--muted) / 35%);
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
word-break: break-all;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
}
|
||||
.trace-snake-node.point-right::after,
|
||||
.trace-snake-node.point-left::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 23px;
|
||||
width: 28px;
|
||||
height: 2px;
|
||||
background: hsl(var(--primary) / 45%);
|
||||
}
|
||||
.trace-snake-node.point-right::after {
|
||||
right: -32px;
|
||||
}
|
||||
.trace-snake-node.point-right::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 19px;
|
||||
right: -36px;
|
||||
border-top: 5px solid transparent;
|
||||
border-bottom: 5px solid transparent;
|
||||
border-left: 7px solid hsl(var(--primary) / 70%);
|
||||
}
|
||||
.trace-snake-node.point-left::after {
|
||||
left: -32px;
|
||||
}
|
||||
.trace-snake-node.point-left::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 19px;
|
||||
left: -36px;
|
||||
border-top: 5px solid transparent;
|
||||
border-bottom: 5px solid transparent;
|
||||
border-right: 7px solid hsl(var(--primary) / 70%);
|
||||
}
|
||||
.trace-snake-turn {
|
||||
position: absolute;
|
||||
bottom: -36px;
|
||||
width: 2px;
|
||||
height: 36px;
|
||||
transform: translateX(-1px);
|
||||
background: hsl(var(--primary) / 45%);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 商品订单卡片看板(C 端紧凑排版 + Apple 毛玻璃):与列表视图展示同一批字段,仅排版不同
|
||||
* 商品订单卡片看板:展示字段与列表视图一致,只改排版。
|
||||
*
|
||||
* 排版语言参考 C 端 App 订单列表(外卖/电商):
|
||||
* - 多列自适应网格(minmax 自动 1-3 列),单卡窄而紧凑,减少纵向占用
|
||||
* - 卡片:毛玻璃 + 14px 圆角 + hairline 细边框;状态用「彩点 + 彩字」
|
||||
* - 商品行右侧直接放合计金额(C 端惯例),人员/收货信息压成内联小字行
|
||||
* - 语义标签:tinted 小胶囊(同色低透明度底,无边框)
|
||||
* 尺寸对齐配送仓库「我的订单」卡片(宽卡、分区留白、最多三列),
|
||||
* 但字段不换成仓库那套(仍保留诊所/人员/金额分项/推广员/包邮等)。
|
||||
*
|
||||
* 数据自取:props.formValues 变化时重置到第 1 页,触底自动加载下一页。
|
||||
* 滚动:不再用内层无限滚动。内层滚动 + 触底追加会和页面滚动抢滚轮,
|
||||
* 卡片悬停上移还会在滚动时反复进出命中区,表现为抽搐。这里改成分页,由页面统一滚动。
|
||||
* 行操作复用父级 buildRowActions(与列表 #action 槽同一份定义)。
|
||||
*/
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { Button, Empty, Image, message, Spin, Switch } from 'ant-design-vue';
|
||||
import { Button, Empty, Image, message, Pagination, Spin, Switch } from 'ant-design-vue';
|
||||
|
||||
import { CardActions } from '#/components/table-action';
|
||||
import SensitiveText from '#/components/sensitive-text/SensitiveText.vue';
|
||||
@@ -60,19 +58,15 @@ const emit = defineEmits<{
|
||||
|
||||
const items = ref<any[]>([]);
|
||||
const page = ref(1);
|
||||
const pageCount = ref(1);
|
||||
const pageSize = ref(12);
|
||||
const total = ref(0);
|
||||
const loading = ref(false);
|
||||
const loadingMore = ref(false);
|
||||
/** 请求序号:只应用最后一次响应,避免快速改筛选时旧结果覆盖新列表 */
|
||||
let requestSeq = 0;
|
||||
/** 已手动展开商品明细的订单 id 集合 */
|
||||
const expandedIds = ref<Set<number>>(new Set());
|
||||
|
||||
const scrollEl = ref<HTMLElement | null>(null);
|
||||
const sentinelEl = ref<HTMLElement | null>(null);
|
||||
let observer: IntersectionObserver | null = null;
|
||||
|
||||
const hasMore = computed(() => page.value < pageCount.value);
|
||||
|
||||
/** 处方类型元数据(全量 0-7,含特色方),无处方类型时返回 null */
|
||||
function prescriptionTypeMeta(row: Record<string, any>) {
|
||||
@@ -201,26 +195,22 @@ function toggleExpand(row: Record<string, any>) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉取一页数据;reset 时清空列表并回到第 1 页
|
||||
* 用 requestSeq 丢弃过期响应,防止筛选连点时旧条件结果覆盖新条件
|
||||
* 拉取当前页。分页而不是追加,避免列表高度在滚动中突变。
|
||||
* requestSeq 丢弃过期响应,防止筛选连点时旧条件结果覆盖新列表。
|
||||
*/
|
||||
async function fetchPage(targetPage: number, reset = false) {
|
||||
async function fetchPage(targetPage: number) {
|
||||
const seq = ++requestSeq;
|
||||
if (reset) {
|
||||
loading.value = true;
|
||||
} else {
|
||||
loadingMore.value = true;
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getOrderList({
|
||||
page: targetPage,
|
||||
pageSize: 12,
|
||||
pageSize: pageSize.value,
|
||||
...(props.formValues || {}),
|
||||
});
|
||||
if (seq !== requestSeq) return;
|
||||
items.value = reset ? (res?.items ?? []) : [...items.value, ...(res?.items ?? [])];
|
||||
items.value = res?.items ?? [];
|
||||
page.value = res?.page ?? targetPage;
|
||||
pageCount.value = res?.page_count ?? 1;
|
||||
total.value = Number(res?.total ?? 0);
|
||||
} catch (error) {
|
||||
if (seq !== requestSeq) return;
|
||||
console.error(error);
|
||||
@@ -228,7 +218,6 @@ async function fetchPage(targetPage: number, reset = false) {
|
||||
} finally {
|
||||
if (seq === requestSeq) {
|
||||
loading.value = false;
|
||||
loadingMore.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -237,55 +226,28 @@ async function fetchPage(targetPage: number, reset = false) {
|
||||
async function reload() {
|
||||
page.value = 1;
|
||||
items.value = [];
|
||||
await fetchPage(1, true);
|
||||
await fetchPage(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作成功后的原地刷新:保留当前页码,避免退款/发货后跳回第 1 页
|
||||
* 卡片是累积加载的,这里折衷为按已加载条数一次性重拉(页码 1、pageSize=已加载数)
|
||||
* 操作成功后的原地刷新:留在当前页,避免退款/发货后跳回第 1 页
|
||||
*/
|
||||
async function refreshKeepPage() {
|
||||
const loaded = Math.max(items.value.length, 12);
|
||||
const seq = ++requestSeq;
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getOrderList({
|
||||
page: 1,
|
||||
pageSize: loaded,
|
||||
...(props.formValues || {}),
|
||||
});
|
||||
if (seq !== requestSeq) return;
|
||||
items.value = res?.items ?? [];
|
||||
// 折算页数:保持触底加载能继续接在后面
|
||||
page.value = Math.ceil((res?.items?.length ?? 0) / 12) || 1;
|
||||
pageCount.value = Math.ceil((res?.total ?? 0) / 12) || 1;
|
||||
} catch (error) {
|
||||
if (seq === requestSeq) console.error(error);
|
||||
} finally {
|
||||
if (seq === requestSeq) loading.value = false;
|
||||
}
|
||||
await fetchPage(page.value);
|
||||
}
|
||||
|
||||
defineExpose({ reload, refreshKeepPage });
|
||||
|
||||
async function loadMore() {
|
||||
if (!hasMore.value || loading.value || loadingMore.value) return;
|
||||
await fetchPage(page.value + 1, false);
|
||||
}
|
||||
|
||||
/** 触底哨兵:滚动到底部附近自动加载下一页 */
|
||||
function setupObserver() {
|
||||
observer?.disconnect();
|
||||
if (!sentinelEl.value) return;
|
||||
observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries.some((e) => e.isIntersecting)) {
|
||||
void loadMore();
|
||||
}
|
||||
},
|
||||
{ root: scrollEl.value, rootMargin: '120px' },
|
||||
);
|
||||
observer.observe(sentinelEl.value);
|
||||
/** 翻页后回到卡片区顶部,避免停在页面最底部看不到新的一页 */
|
||||
async function onPageChange(nextPage: number, nextSize: number) {
|
||||
if (nextSize !== pageSize.value) {
|
||||
pageSize.value = nextSize;
|
||||
page.value = 1;
|
||||
} else {
|
||||
page.value = nextPage;
|
||||
}
|
||||
await fetchPage(page.value);
|
||||
scrollEl.value?.scrollIntoView({ block: 'start' });
|
||||
}
|
||||
|
||||
watch(
|
||||
@@ -296,15 +258,8 @@ watch(
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
await reload();
|
||||
await nextTick();
|
||||
setupObserver();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
observer?.disconnect();
|
||||
observer = null;
|
||||
onMounted(() => {
|
||||
void reload();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -313,8 +268,6 @@ onBeforeUnmount(() => {
|
||||
<Spin :spinning="loading">
|
||||
<div v-if="items.length" class="po-card-grid">
|
||||
<div v-for="row in items" :key="row.id" class="po-card">
|
||||
<!-- 角落径向荧光(与提现管理统计岛同款质感装饰) -->
|
||||
<div class="po-card__glow" />
|
||||
<!-- 行1 头部:诊所名(可点)+ 状态彩点 -->
|
||||
<div class="po-card__head">
|
||||
<button
|
||||
@@ -583,86 +536,65 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
<Empty v-else-if="!loading" description="暂无订单" />
|
||||
</Spin>
|
||||
<div ref="sentinelEl" class="h-8"></div>
|
||||
<div v-if="loadingMore" class="py-3 text-center text-xs text-[hsl(var(--muted-foreground))]">
|
||||
加载中…
|
||||
</div>
|
||||
<div
|
||||
v-else-if="items.length && !hasMore"
|
||||
class="py-3 text-center text-xs text-[hsl(var(--muted-foreground))]"
|
||||
>
|
||||
没有更多了
|
||||
<div v-if="total > 0" class="po-card-pager">
|
||||
<Pagination
|
||||
:current="page"
|
||||
:page-size="pageSize"
|
||||
:page-size-options="['12', '24', '48']"
|
||||
:total="total"
|
||||
show-quick-jumper
|
||||
show-size-changer
|
||||
@change="onPageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.po-card-scroll {
|
||||
height: 100%;
|
||||
/* 左右留 6px:卡片 hover 阴影不被容器裁出切边 */
|
||||
padding: 4px 6px 20px;
|
||||
overflow: auto;
|
||||
padding: 4px 2px 8px;
|
||||
}
|
||||
|
||||
/* 多列自适应网格:单卡最窄 400px,宽屏自动 2-3 列(C 端紧凑卡片密度);
|
||||
min(400px, 100%) 防止窄容器(分屏)时轨道小于容器导致横向滚动 */
|
||||
/* 与配送仓库订单相同的列数:窄屏 1 列,中屏 2 列,宽屏最多 3 列,卡片随列宽变大 */
|
||||
.po-card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(min(400px, 100%), 1fr));
|
||||
gap: 10px;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
/* 玻璃质感紧凑卡片(与提现管理统计岛同款):主色渐变玻璃底 + 12px 圆角 + 1px 边框 */
|
||||
@media (min-width: 768px) {
|
||||
.po-card-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.po-card-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
/* 不用位移、不用毛玻璃:滚动经过卡片时 hover 上移会反复触发重绘,看起来像抽搐 */
|
||||
.po-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
hsl(var(--primary) / 4%),
|
||||
hsl(var(--card, var(--background)))
|
||||
);
|
||||
backdrop-filter: blur(8px);
|
||||
background: hsl(var(--card, var(--background)));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 12px;
|
||||
transition:
|
||||
transform 0.2s ease,
|
||||
box-shadow 0.2s ease,
|
||||
border-color 0.2s ease;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 1px 2px hsl(var(--foreground) / 6%);
|
||||
}
|
||||
|
||||
.po-card:hover {
|
||||
border-color: hsl(var(--primary) / 40%);
|
||||
box-shadow:
|
||||
0 8px 20px hsl(var(--primary) / 14%),
|
||||
0 2px 6px rgb(0 0 0 / 6%);
|
||||
transform: translateY(-3px);
|
||||
border-color: hsl(var(--primary) / 35%);
|
||||
box-shadow: 0 0 6px hsl(var(--primary) / 18%);
|
||||
}
|
||||
|
||||
/* 角落径向荧光:右上角淡主色光斑,hover 增强 */
|
||||
.po-card__glow {
|
||||
position: absolute;
|
||||
top: -40px;
|
||||
right: -40px;
|
||||
width: 110px;
|
||||
height: 110px;
|
||||
pointer-events: none;
|
||||
background: radial-gradient(circle, hsl(var(--primary) / 18%) 0%, transparent 70%);
|
||||
opacity: 0.4;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.po-card:hover .po-card__glow {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 行1 头部:店铺 + 状态彩点 */
|
||||
.po-card__head {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
padding: 10px 12px 0;
|
||||
padding: 16px 20px 12px;
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
.po-card__store {
|
||||
@@ -678,9 +610,8 @@ onBeforeUnmount(() => {
|
||||
|
||||
.po-card__store-name {
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.2px;
|
||||
color: hsl(var(--foreground));
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
@@ -692,33 +623,32 @@ onBeforeUnmount(() => {
|
||||
|
||||
.po-card__store-arrow {
|
||||
flex-shrink: 0;
|
||||
font-size: 14px;
|
||||
font-size: 16px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.po-card__status {
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
gap: 4px;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
font-size: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.po-card__status-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* 行2 单号/时间小灰字 */
|
||||
.po-card__meta {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
padding: 1px 12px 0;
|
||||
font-size: 11px;
|
||||
padding: 12px 20px 0;
|
||||
font-size: 13px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
@@ -726,45 +656,41 @@ onBeforeUnmount(() => {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* tinted 胶囊行(紧凑小号) */
|
||||
.po-card__pills {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
padding: 6px 12px 0;
|
||||
gap: 6px;
|
||||
padding: 10px 20px 0;
|
||||
}
|
||||
|
||||
.po-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 1px 7px;
|
||||
font-size: 10px;
|
||||
padding: 2px 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
line-height: 20px;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.po-pill--action {
|
||||
cursor: pointer;
|
||||
transition: filter 0.15s ease;
|
||||
}
|
||||
|
||||
.po-pill--action:hover {
|
||||
filter: brightness(1.08) saturate(1.2);
|
||||
filter: brightness(1.08);
|
||||
}
|
||||
|
||||
/* 行3 商品行:左图文右合计(C 端订单卡惯例) */
|
||||
.po-card__goods {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
padding: 7px 10px;
|
||||
margin: 8px 12px 0;
|
||||
padding: 12px;
|
||||
margin: 12px 20px 0;
|
||||
cursor: pointer;
|
||||
background: hsl(var(--muted) / 30%);
|
||||
border-radius: 10px;
|
||||
transition: background-color 0.2s ease;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.po-card__goods:hover {
|
||||
@@ -776,12 +702,12 @@ onBeforeUnmount(() => {
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
overflow: hidden;
|
||||
background: hsl(var(--card, var(--background)));
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 2px rgb(0 0 0 / 6%);
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.po-card__goods-thumb :deep(img) {
|
||||
@@ -789,14 +715,14 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.po-card__goods-placeholder {
|
||||
font-size: 11px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.po-card__goods-name {
|
||||
overflow: hidden;
|
||||
font-size: 13px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--foreground));
|
||||
text-overflow: ellipsis;
|
||||
@@ -805,10 +731,10 @@ onBeforeUnmount(() => {
|
||||
|
||||
.po-card__goods-count {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-top: 1px;
|
||||
font-size: 11px;
|
||||
margin-top: 6px;
|
||||
font-size: 13px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
@@ -817,38 +743,34 @@ onBeforeUnmount(() => {
|
||||
flex-shrink: 0;
|
||||
gap: 1px;
|
||||
align-items: center;
|
||||
font-size: 11px;
|
||||
font-size: 13px;
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.po-card__goods-toggle i {
|
||||
font-style: normal;
|
||||
transform: rotate(90deg);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.po-card__goods-toggle i.up {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
/* 合计金额(商品行右侧) */
|
||||
.po-card__amount {
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.po-card__amount-value {
|
||||
/* DIN 等宽数字:与提现管理统计岛金额同款字体质感 */
|
||||
font-family: 'DIN Alternate', ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: 16px;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.3px;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.po-card__amount-value i {
|
||||
margin-right: 1px;
|
||||
font-size: 11px;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -861,40 +783,39 @@ onBeforeUnmount(() => {
|
||||
.po-card__chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
padding: 6px 12px 0;
|
||||
gap: 6px;
|
||||
padding: 10px 20px 0;
|
||||
}
|
||||
|
||||
.po-card__drug-chip {
|
||||
padding: 1px 8px;
|
||||
font-size: 11px;
|
||||
padding: 2px 10px;
|
||||
font-size: 13px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
background: hsl(var(--muted) / 35%);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
/* 行4 人员内联行:label 小灰字 + 可点链接,一行自动换行 */
|
||||
.po-card__people {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 2px 14px;
|
||||
padding: 7px 12px 0;
|
||||
font-size: 12px;
|
||||
gap: 6px 16px;
|
||||
padding: 12px 20px 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.po-kv {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
gap: 6px;
|
||||
align-items: baseline;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.po-kv > i {
|
||||
flex-shrink: 0;
|
||||
font-size: 11px;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
color: hsl(var(--muted-foreground) / 75%);
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.po-kv > em {
|
||||
@@ -906,10 +827,10 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.po-link {
|
||||
max-width: 140px;
|
||||
max-width: 180px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
font-size: 14px;
|
||||
color: hsl(var(--primary));
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
@@ -922,56 +843,61 @@ onBeforeUnmount(() => {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* 行5/6 收货、推广员内联行:label + 单行截断 */
|
||||
.po-card__express {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
padding: 4px 12px 0;
|
||||
font-size: 12px;
|
||||
padding: 8px 20px 0;
|
||||
font-size: 14px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.po-card__express > i {
|
||||
flex-shrink: 0;
|
||||
font-size: 11px;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
color: hsl(var(--muted-foreground) / 75%);
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.po-card__salesperson-status {
|
||||
flex-shrink: 0;
|
||||
padding: 0 7px;
|
||||
font-size: 10px;
|
||||
line-height: 16px;
|
||||
padding: 0 8px;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
background: hsl(var(--muted) / 40%);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
/* 弹性占位:吃掉剩余高度让 footer 吸底 */
|
||||
.po-card__spacer {
|
||||
flex: 1 1 auto;
|
||||
min-height: 8px;
|
||||
min-height: 12px;
|
||||
}
|
||||
|
||||
/* 行7 footer:左分项/包邮,右操作按钮;hairline 分隔 */
|
||||
.po-card__footer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px 10px;
|
||||
gap: 8px 12px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 4px 6px 4px 12px;
|
||||
border-top: 1px solid hsl(var(--border) / 50%);
|
||||
padding: 12px 12px 12px 20px;
|
||||
margin-top: 12px;
|
||||
border-top: 1px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
.po-card__amount-parts {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
font-size: 11px;
|
||||
font-size: 13px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.po-card-pager {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 20px 0 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -102,6 +102,7 @@ export const gridOptions: VxeGridProps<RowType> = {
|
||||
],
|
||||
keepSource: true,
|
||||
pagerConfig: {},
|
||||
height: 'auto',
|
||||
columnConfig: {
|
||||
useKey: true,
|
||||
},
|
||||
|
||||
@@ -1184,20 +1184,21 @@ function buildRowActions(row: Record<string, any>): {
|
||||
<PrescriptionDetailModal />
|
||||
<TraceDrawer />
|
||||
<ChinaErpSyncDrawer @synced="() => refreshCurrentView()" />
|
||||
<!-- 紧凑统计岛:一行 4 格(仅占 2 格),设计对齐提现管理账户卡统计岛 -->
|
||||
<StatIslands class="mb-3" :items="overviewItems" :columns="4" />
|
||||
<!-- 统计、搜索固定高度,列表/卡片只留一个滚动层,避免页面和表格同时滚动时抢滚轮 -->
|
||||
<div class="flex h-full min-h-0 flex-col overflow-hidden">
|
||||
<StatIslands class="mb-3 shrink-0" :items="overviewItems" :columns="4" />
|
||||
<!-- 顶部统一搜索(双视图共用,替代原 grid 内嵌表单) -->
|
||||
<div class="mb-3 rounded-lg border border-[hsl(var(--border))] bg-[hsl(var(--card,var(--background)))] p-3">
|
||||
<div class="mb-3 shrink-0 rounded-lg border border-[hsl(var(--border))] bg-[hsl(var(--card,var(--background)))] p-3">
|
||||
<SearchForm />
|
||||
</div>
|
||||
<!-- 视图切换:列表 / 卡片看板;卡片模式下补充导出入口(列表模式导出在表格工具栏) -->
|
||||
<div class="mb-3 flex items-center justify-between gap-3">
|
||||
<div class="mb-3 flex shrink-0 items-center justify-between gap-3">
|
||||
<ViewModeSwitch v-model="viewMode" />
|
||||
<Button v-if="viewMode === 'card'" type="primary" @click="openExportModal">
|
||||
导出
|
||||
</Button>
|
||||
</div>
|
||||
<Grid v-if="viewMode === 'list'">
|
||||
<Grid v-if="viewMode === 'list'" class="h-0 min-h-0 flex-1">
|
||||
<template #toolbar-actions>
|
||||
<TableAction
|
||||
:flex="false"
|
||||
@@ -1536,7 +1537,7 @@ function buildRowActions(row: Record<string, any>): {
|
||||
</template>
|
||||
</Grid>
|
||||
<!-- 卡片看板视图:展示内容与列表一致,行操作/弹窗全部复用父级逻辑 -->
|
||||
<div v-else class="po-card-wrap">
|
||||
<div v-else class="min-h-0 flex-1 overflow-y-auto">
|
||||
<ProductOrderCardList
|
||||
ref="cardListRef"
|
||||
:form-values="searchValues"
|
||||
@@ -1558,6 +1559,7 @@ function buildRowActions(row: Record<string, any>): {
|
||||
@reverse-salesperson="handleReverseSalesperson"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<PercentAdjustDrawer />
|
||||
<!-- 订单改仓:按药分别选仓 -->
|
||||
<AntdModal
|
||||
@@ -1645,10 +1647,4 @@ function buildRowActions(row: Record<string, any>): {
|
||||
.tcm-drug-chip {
|
||||
box-shadow: 0 1px 2px rgb(0 0 0 / 4%);
|
||||
}
|
||||
|
||||
/* 卡片看板容器:撑满剩余高度,内部滚动(统计岛 + 收起态搜索一行 + 切换行约 220px,min-height 兜底) */
|
||||
.po-card-wrap {
|
||||
height: calc(100% - 220px);
|
||||
min-height: 420px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -13,6 +13,7 @@ import { modalFormProps } from '../config/form';
|
||||
const isUpdate = ref(false);
|
||||
const gridApi = ref();
|
||||
const getMyInfoApi = ref();
|
||||
const existingUrls = ref<string[]>([]);
|
||||
|
||||
const [Form, formApi] = useVbenForm(modalFormProps);
|
||||
|
||||
@@ -25,30 +26,47 @@ const [Modal, modalApi] = useVbenModal({
|
||||
onConfirm: async () => {
|
||||
const e = await formApi.validate();
|
||||
if (!e.valid) return;
|
||||
const values = await formApi.getValues();
|
||||
modalApi.setState({ loading: true, confirmLoading: true });
|
||||
const submitApi = saveNavUrlApi;
|
||||
submitApi(values)
|
||||
.then(() => {
|
||||
message.success('保存成功');
|
||||
gridApi.value?.reload();
|
||||
getMyInfoApi.value();
|
||||
modalApi.close();
|
||||
})
|
||||
.finally(() => {
|
||||
modalApi.setState({ loading: false, confirmLoading: false });
|
||||
});
|
||||
|
||||
const values = await formApi.getValues();
|
||||
modalApi.setState({ loading: true, confirmLoading: true });
|
||||
try {
|
||||
// 新图拼在旧图后面,避免 uploadNav 全量同步时误删已有轮播
|
||||
const nextUrls = Array.isArray(values.url) ? values.url : [];
|
||||
const merged = [...existingUrls.value];
|
||||
for (const u of nextUrls) {
|
||||
const pic = String(u || '').trim();
|
||||
if (pic && !merged.includes(pic)) {
|
||||
merged.push(pic);
|
||||
}
|
||||
}
|
||||
await saveNavUrlApi({ url: merged });
|
||||
message.success('保存成功');
|
||||
gridApi.value?.reload?.();
|
||||
getMyInfoApi.value?.();
|
||||
modalApi.close();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
modalApi.setState({ loading: false, confirmLoading: false });
|
||||
}
|
||||
},
|
||||
onOpenChange(isOpen: boolean) {
|
||||
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
|
||||
if (isOpen) {
|
||||
getMyInfoApi.value = isOpen ? modalApi.getData()?.getMyInfo : null;
|
||||
const { values, update } = modalApi.getData<Record<string, any>>();
|
||||
const payload = modalApi.getData<Record<string, any>>() || {};
|
||||
existingUrls.value = Array.isArray(payload.existingUrls)
|
||||
? payload.existingUrls.map(String).filter(Boolean)
|
||||
: [];
|
||||
const { values, update } = payload;
|
||||
if (values) {
|
||||
isUpdate.value = update;
|
||||
formApi.setValues(values);
|
||||
} else {
|
||||
isUpdate.value = false;
|
||||
formApi.reset?.();
|
||||
}
|
||||
} else {
|
||||
existingUrls.value = [];
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -85,9 +85,13 @@ const [UploadModals, UploadModalApi] = useVbenModal({
|
||||
});
|
||||
|
||||
const openUpload = () => {
|
||||
// uploadNav 是全量同步:必须把已有图一并带上,否则「只传新图」会把旧轮播软删掉
|
||||
const existingUrls = (data.value?.nav || [])
|
||||
.map((item) => String(item?.pic || '').trim())
|
||||
.filter(Boolean);
|
||||
UploadModalApi.setData({
|
||||
// doctorId,
|
||||
getMyInfo,
|
||||
existingUrls,
|
||||
});
|
||||
UploadModalApi.open();
|
||||
};
|
||||
|
||||
@@ -45,6 +45,18 @@ export async function getProductListDoctorReception(data: any) {
|
||||
return requestClient.get<any>(`${prefix}product-list`, { params: data });
|
||||
}
|
||||
|
||||
/**
|
||||
* 复用/常用方填方前对比中药快照价与当前门店价
|
||||
* 为什么:供货比例变更后关系表已更新,快照价不一致时要让医生确认是否按最新价计算
|
||||
*/
|
||||
export async function compareChineseDrugPricesApi(data: {
|
||||
register_id?: number;
|
||||
store_id?: number;
|
||||
drugs: Array<Record<string, any>>;
|
||||
}) {
|
||||
return requestClient.post<any>(`${prefix}compare-chinese-drug-prices`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取中药规格
|
||||
* @param data
|
||||
@@ -670,3 +682,23 @@ export async function getProxyPayQrcodeApi(orderId: number) {
|
||||
order_id: orderId,
|
||||
});
|
||||
}
|
||||
|
||||
/** 处方图 OCR(DeepSeek) */
|
||||
export async function ocrPrescriptionImageApi(data: {
|
||||
image_urls: string[];
|
||||
mode: 'reception' | 'transfer';
|
||||
register_id?: number;
|
||||
store_id?: number;
|
||||
}) {
|
||||
return requestClient.post<any>(`${prefix}ocr-prescription-image`, data);
|
||||
}
|
||||
|
||||
/** 确认识图传方:建患者/挂号 + 待导入传方单 */
|
||||
export async function confirmTransferImageOcrApi(data: Record<string, any>) {
|
||||
return requestClient.post<{
|
||||
transfer_id: number;
|
||||
register_id: number;
|
||||
user_patient_id: number;
|
||||
patient_action: string;
|
||||
}>(`${prefix}confirm-transfer-image-ocr`, data);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 中药制剂方式 / 代煎 / 用量配置
|
||||
* 与小程序对齐:标题带必填星号;父页提交失败可传 errorKeys 标红
|
||||
*/
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { MinusOutlined, PlusOutlined } from '@ant-design/icons-vue';
|
||||
import { InputNumber } from 'ant-design-vue';
|
||||
|
||||
@@ -13,19 +19,30 @@ export type ChineseMedicineConfigChange = {
|
||||
dayDosage?: number;
|
||||
};
|
||||
|
||||
const props = defineProps<{
|
||||
ruleType: number;
|
||||
packageMethodId?: number | null;
|
||||
processRuleId?: number | null;
|
||||
childProcessRuleId?: number | null;
|
||||
processRuleNoteId?: number | null;
|
||||
dosage: number;
|
||||
dayDosage: number;
|
||||
processRuleList?: Array<{ id: number; name: string }>;
|
||||
childProcessRuleList?: Array<{ id: number; name: string }>;
|
||||
processRuleNoteList?: Array<{ id: number; note?: string; name?: string }>;
|
||||
disabled?: boolean;
|
||||
}>();
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
ruleType: number;
|
||||
packageMethodId?: number | null;
|
||||
processRuleId?: number | null;
|
||||
childProcessRuleId?: number | null;
|
||||
processRuleNoteId?: number | null;
|
||||
dosage: number;
|
||||
dayDosage: number;
|
||||
processRuleList?: Array<{ id: number; name: string }>;
|
||||
childProcessRuleList?: Array<{ id: number; name: string }>;
|
||||
processRuleNoteList?: Array<{ id: number; note?: string; name?: string }>;
|
||||
disabled?: boolean;
|
||||
/** 未填字段名;父页提交失败传入,改选后由父清空 */
|
||||
errorKeys?: string[];
|
||||
}>(),
|
||||
{
|
||||
processRuleList: () => [],
|
||||
childProcessRuleList: () => [],
|
||||
processRuleNoteList: () => [],
|
||||
disabled: false,
|
||||
errorKeys: () => [],
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
change: [payload: ChineseMedicineConfigChange];
|
||||
@@ -37,6 +54,23 @@ const packageMethodList = [
|
||||
{ id: 2, name: '剂包' },
|
||||
];
|
||||
|
||||
/** 未填字段 → 区块标红 */
|
||||
const errorMap = computed(() => {
|
||||
const map: Record<string, boolean> = {
|
||||
ruleType: false,
|
||||
packageMethodId: false,
|
||||
processRuleId: false,
|
||||
childProcessRuleId: false,
|
||||
processRuleNoteId: false,
|
||||
dosage: false,
|
||||
dayDosage: false,
|
||||
};
|
||||
for (const k of props.errorKeys || []) {
|
||||
if (Object.prototype.hasOwnProperty.call(map, k)) map[k] = true;
|
||||
}
|
||||
return map;
|
||||
});
|
||||
|
||||
/**
|
||||
* 统一向父组件抛出配置变更
|
||||
* 父组件负责级联加载制剂/煎法/备注列表
|
||||
@@ -108,9 +142,12 @@ function updateDayDosage(val: number) {
|
||||
|
||||
<template>
|
||||
<div class="chinese-medicine-config">
|
||||
<!-- 制剂方式 -->
|
||||
<div class="config-section mb-4">
|
||||
<div class="section-title mb-2 text-sm font-bold text-gray-800 dark:text-gray-200">制剂方式</div>
|
||||
<!-- 制剂方式(必填) -->
|
||||
<div class="config-section mb-4" :class="{ 'sec-error': errorMap.ruleType }">
|
||||
<div class="section-title mb-2">
|
||||
<span class="title-text">制剂方式</span>
|
||||
<span class="req-star">*</span>
|
||||
</div>
|
||||
<div class="pill-group">
|
||||
<span
|
||||
class="pill"
|
||||
@@ -123,11 +160,19 @@ function updateDayDosage(val: number) {
|
||||
@click="handleRuleTypeChange(2)"
|
||||
>委托调剂</span>
|
||||
</div>
|
||||
<div v-if="errorMap.ruleType" class="err-tip">请选择制剂方式</div>
|
||||
</div>
|
||||
|
||||
<!-- 自制剂:包法 -->
|
||||
<div v-if="ruleType === 1" class="config-section mb-4 border-t border-dashed border-gray-200 dark:border-gray-700 pt-4">
|
||||
<div class="section-title mb-2 text-sm font-bold text-gray-800 dark:text-gray-200">包法选择</div>
|
||||
<div
|
||||
v-if="ruleType === 1"
|
||||
class="config-section mb-4 border-t border-dashed border-border pt-4"
|
||||
:class="{ 'sec-error': errorMap.packageMethodId }"
|
||||
>
|
||||
<div class="section-title mb-2">
|
||||
<span class="title-text">包法选择</span>
|
||||
<span class="req-star">*</span>
|
||||
</div>
|
||||
<div class="pill-group">
|
||||
<span
|
||||
v-for="item in packageMethodList"
|
||||
@@ -137,12 +182,19 @@ function updateDayDosage(val: number) {
|
||||
@click="handlePackageMethodClick(item.id)"
|
||||
>{{ item.name }}</span>
|
||||
</div>
|
||||
<div v-if="errorMap.packageMethodId" class="err-tip">请选择包法</div>
|
||||
</div>
|
||||
|
||||
<!-- 委托调剂:制剂 / 煎法 / 备注 -->
|
||||
<template v-if="ruleType === 2">
|
||||
<div class="config-section mb-4 border-t border-dashed border-gray-200 dark:border-gray-700 pt-4">
|
||||
<div class="section-title mb-2 text-sm font-bold text-gray-800 dark:text-gray-200">制剂要求</div>
|
||||
<div
|
||||
class="config-section mb-4 border-t border-dashed border-border pt-4"
|
||||
:class="{ 'sec-error': errorMap.processRuleId }"
|
||||
>
|
||||
<div class="section-title mb-2">
|
||||
<span class="title-text">制剂要求</span>
|
||||
<span class="req-star">*</span>
|
||||
</div>
|
||||
<div class="pill-group">
|
||||
<span
|
||||
v-for="item in processRuleList"
|
||||
@@ -152,9 +204,17 @@ function updateDayDosage(val: number) {
|
||||
@click="handleProcessRuleClick(item)"
|
||||
>{{ item.name }}</span>
|
||||
</div>
|
||||
<div v-if="errorMap.processRuleId" class="err-tip">请选择制剂要求</div>
|
||||
</div>
|
||||
<div v-if="childProcessRuleList?.length" class="config-section mb-4">
|
||||
<div class="section-title mb-2 text-sm font-bold text-gray-800 dark:text-gray-200">煎法说明</div>
|
||||
<div
|
||||
v-if="childProcessRuleList?.length"
|
||||
class="config-section mb-4"
|
||||
:class="{ 'sec-error': errorMap.childProcessRuleId }"
|
||||
>
|
||||
<div class="section-title mb-2">
|
||||
<span class="title-text">煎法说明</span>
|
||||
<span class="req-star">*</span>
|
||||
</div>
|
||||
<div class="pill-group">
|
||||
<span
|
||||
v-for="item in childProcessRuleList"
|
||||
@@ -164,9 +224,17 @@ function updateDayDosage(val: number) {
|
||||
@click="handleChildProcessRuleClick(item)"
|
||||
>{{ item.name }}</span>
|
||||
</div>
|
||||
<div v-if="errorMap.childProcessRuleId" class="err-tip">请选择煎法说明</div>
|
||||
</div>
|
||||
<div v-if="processRuleNoteList?.length" class="config-section mb-4">
|
||||
<div class="section-title mb-2 text-sm font-bold text-gray-800 dark:text-gray-200">附加备注</div>
|
||||
<div
|
||||
v-if="processRuleNoteList?.length"
|
||||
class="config-section mb-4"
|
||||
:class="{ 'sec-error': errorMap.processRuleNoteId }"
|
||||
>
|
||||
<div class="section-title mb-2">
|
||||
<span class="title-text">附加备注</span>
|
||||
<span class="req-star">*</span>
|
||||
</div>
|
||||
<div class="pill-group">
|
||||
<span
|
||||
v-for="item in processRuleNoteList"
|
||||
@@ -176,15 +244,16 @@ function updateDayDosage(val: number) {
|
||||
@click="handleProcessRuleNoteClick(item)"
|
||||
>{{ item.note || item.name }}</span>
|
||||
</div>
|
||||
<div v-if="errorMap.processRuleNoteId" class="err-tip">请选择附加备注</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 剂数与频次 -->
|
||||
<div class="config-section border-t border-dashed border-gray-200 dark:border-gray-700 pt-4">
|
||||
<div class="config-section border-t border-dashed border-border pt-4">
|
||||
<div class="mt-2 flex flex-wrap items-center gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm font-bold text-gray-800 dark:text-gray-200">剂数</span>
|
||||
<!-- 带加减按钮时需足够宽度,避免中间输入区过窄无法输入 -->
|
||||
<div class="flex items-center gap-2" :class="{ 'field-error': errorMap.dosage }">
|
||||
<span class="title-text">剂数</span>
|
||||
<span class="req-star">*</span>
|
||||
<InputNumber
|
||||
:value="dosage"
|
||||
:disabled="disabled"
|
||||
@@ -200,11 +269,11 @@ function updateDayDosage(val: number) {
|
||||
<PlusOutlined @click="!disabled && updateDosage(dosage < 100 ? dosage + 1 : 100)" />
|
||||
</template>
|
||||
</InputNumber>
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400">剂(天)</span>
|
||||
<span class="text-sm text-muted-foreground">剂(天)</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm font-bold text-gray-800 dark:text-gray-200">用药频次</span>
|
||||
<!-- 同上:加宽以保证可正常手输数字 -->
|
||||
<div class="flex items-center gap-2" :class="{ 'field-error': errorMap.dayDosage }">
|
||||
<span class="title-text">用药频次</span>
|
||||
<span class="req-star">*</span>
|
||||
<InputNumber
|
||||
:value="dayDosage"
|
||||
:disabled="disabled"
|
||||
@@ -220,14 +289,52 @@ function updateDayDosage(val: number) {
|
||||
<PlusOutlined @click="!disabled && updateDayDosage(dayDosage < 100 ? dayDosage + 1 : 100)" />
|
||||
</template>
|
||||
</InputNumber>
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400">次/天</span>
|
||||
<span class="text-sm text-muted-foreground">次/天</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="errorMap.dosage" class="err-tip">请填写剂数</div>
|
||||
<div v-if="errorMap.dayDosage" class="err-tip">请填写用药频次</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
.title-text {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
.req-star {
|
||||
margin-left: 2px;
|
||||
color: hsl(var(--destructive, 0 84% 60%));
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
}
|
||||
.err-tip {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: hsl(var(--destructive, 0 84% 60%));
|
||||
}
|
||||
.sec-error {
|
||||
padding: 10px;
|
||||
margin-left: -10px;
|
||||
margin-right: -10px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid hsl(var(--destructive, 0 84% 60%) / 45%);
|
||||
box-shadow: 0 0 6px hsl(var(--destructive, 0 84% 60%) / 18%);
|
||||
background: hsl(var(--destructive, 0 84% 60%) / 6%);
|
||||
}
|
||||
.field-error {
|
||||
padding: 4px 8px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid hsl(var(--destructive, 0 84% 60%) / 45%);
|
||||
box-shadow: 0 0 6px hsl(var(--destructive, 0 84% 60%) / 18%);
|
||||
}
|
||||
.pill-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -235,8 +342,8 @@ function updateDayDosage(val: number) {
|
||||
}
|
||||
.pill {
|
||||
padding: 4px 16px;
|
||||
background-color: #f4f6f8;
|
||||
color: #6c7380;
|
||||
background-color: hsl(var(--muted) / 0.55);
|
||||
color: hsl(var(--muted-foreground));
|
||||
border-radius: 20px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
@@ -245,45 +352,24 @@ function updateDayDosage(val: number) {
|
||||
user-select: none;
|
||||
}
|
||||
.pill.active {
|
||||
background-color: #00a88a;
|
||||
color: #fff;
|
||||
border-color: #00a88a;
|
||||
background-color: hsl(var(--primary));
|
||||
color: hsl(var(--primary-foreground, 0 0% 100%));
|
||||
border-color: hsl(var(--primary));
|
||||
font-weight: 600;
|
||||
}
|
||||
.pill.outline {
|
||||
background-color: #fff;
|
||||
border-color: #e2e8f0;
|
||||
background-color: hsl(var(--card, var(--background)));
|
||||
border-color: hsl(var(--border));
|
||||
}
|
||||
.pill.outline.active {
|
||||
background-color: #e8f6f4;
|
||||
color: #00a88a;
|
||||
border-color: #00a88a;
|
||||
background-color: hsl(var(--primary) / 0.12);
|
||||
color: hsl(var(--primary));
|
||||
border-color: hsl(var(--primary));
|
||||
}
|
||||
.pill.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
/* 暗色模式:标签背景与选中态 */
|
||||
.dark .pill {
|
||||
background-color: #2a2a2a;
|
||||
color: #a3a3a3;
|
||||
}
|
||||
.dark .pill.active {
|
||||
background-color: #00a88a;
|
||||
color: #fff;
|
||||
border-color: #00a88a;
|
||||
}
|
||||
.dark .pill.outline {
|
||||
background-color: #1f1f1f;
|
||||
border-color: #404040;
|
||||
color: #a3a3a3;
|
||||
}
|
||||
.dark .pill.outline.active {
|
||||
background-color: rgba(0, 168, 138, 0.15);
|
||||
color: #3dd6b5;
|
||||
border-color: #00a88a;
|
||||
}
|
||||
/* 剂数/频次输入:原 w-24 过窄,加减按钮占位后中间几乎无法输入 */
|
||||
.dosage-input {
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@
|
||||
* 常用快捷标签(诊断 / 医嘱 / 病历词条)
|
||||
* - 正文已包含该文案时高亮(is-active)
|
||||
* - 点击:已在正文则移除,否则追加(由父级 onEntryPick 统一 toggle)
|
||||
* - collapsible:可收起常用区,避免开方页底部常驻占高
|
||||
*/
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
|
||||
@@ -25,12 +26,18 @@ const props = withDefaults(
|
||||
fieldValue?: string;
|
||||
limit?: number;
|
||||
disabled?: boolean;
|
||||
/** 是否可收起(开方页诊断/医嘱建议开) */
|
||||
collapsible?: boolean;
|
||||
/** 初始是否收起;仅 collapsible 时生效 */
|
||||
defaultCollapsed?: boolean;
|
||||
}>(),
|
||||
{
|
||||
fieldCode: '',
|
||||
fieldValue: '',
|
||||
limit: 12,
|
||||
disabled: false,
|
||||
collapsible: false,
|
||||
defaultCollapsed: false,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -42,6 +49,8 @@ type ChipItem = { id: string | number; text: string };
|
||||
|
||||
const chips = ref<ChipItem[]>([]);
|
||||
const loading = ref(false);
|
||||
/** 收起态:默认不铺开常用标签,点标题再展开 */
|
||||
const collapsed = ref(!!props.defaultCollapsed);
|
||||
|
||||
/** 文案是否已出现在字段值中(逗号分段或整行/包含) */
|
||||
function textInField(fieldValue: string, text: string) {
|
||||
@@ -86,7 +95,6 @@ async function loadChips() {
|
||||
chips.value = [];
|
||||
return;
|
||||
}
|
||||
// 全量 my-entry-list 一次,再按 field_code 本地切片
|
||||
const list = await fetchMyEntriesByField(props.fieldCode, props.limit);
|
||||
chips.value = (list || [])
|
||||
.map((item: any) => {
|
||||
@@ -133,12 +141,22 @@ function onPick(item: ChipItem) {
|
||||
emit('select', item.text);
|
||||
}
|
||||
|
||||
function toggleCollapsed() {
|
||||
if (!props.collapsible) return;
|
||||
collapsed.value = !collapsed.value;
|
||||
}
|
||||
|
||||
const labelText = computed(() => {
|
||||
if (props.source === 'diagnosis') return '常用诊断';
|
||||
if (props.source === 'doctor_order') return '常用医嘱';
|
||||
return '常用词条';
|
||||
});
|
||||
|
||||
const showChipList = computed(() => {
|
||||
if (!props.collapsible) return true;
|
||||
return !collapsed.value;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => [props.source, props.fieldCode] as const,
|
||||
() => {
|
||||
@@ -154,8 +172,17 @@ defineExpose({ reload: reloadChips });
|
||||
</script>
|
||||
<template>
|
||||
<div v-if="chips.length > 0" class="dx-order-chips">
|
||||
<span class="dx-order-chips__label">{{ labelText }}</span>
|
||||
<div class="dx-order-chips__list">
|
||||
<button
|
||||
v-if="collapsible"
|
||||
type="button"
|
||||
class="dx-order-chips__toggle"
|
||||
@click="toggleCollapsed"
|
||||
>
|
||||
<span class="dx-order-chips__label">{{ labelText }}</span>
|
||||
<span class="dx-order-chips__meta">{{ chips.length }} · {{ collapsed ? '展开' : '收起' }}</span>
|
||||
</button>
|
||||
<span v-else class="dx-order-chips__label">{{ labelText }}</span>
|
||||
<div v-if="showChipList" class="dx-order-chips__list">
|
||||
<Tag
|
||||
v-for="item in chips"
|
||||
:key="`${item.id}_${item.text}`"
|
||||
@@ -174,7 +201,7 @@ defineExpose({ reload: reloadChips });
|
||||
</template>
|
||||
<style scoped>
|
||||
.dx-order-chips {
|
||||
margin-top: 8px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.dx-order-chips__label {
|
||||
display: block;
|
||||
@@ -182,6 +209,26 @@ defineExpose({ reload: reloadChips });
|
||||
font-size: 12px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
.dx-order-chips__toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
margin: 0 0 4px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
}
|
||||
.dx-order-chips__toggle .dx-order-chips__label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.dx-order-chips__meta {
|
||||
font-size: 12px;
|
||||
color: hsl(var(--primary));
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.dx-order-chips__list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -2,6 +2,10 @@ import type { VbenFormProps } from '#/adapter/form';
|
||||
|
||||
import { monthToTodayRangeDayjs } from '#/utils/search-time-range';
|
||||
|
||||
/**
|
||||
* 资金流水搜索表单
|
||||
* 保留时间范围,并增加诊所 / 订单号筛选,便于按门店或单号快速定位流水
|
||||
*/
|
||||
export const formOptions: VbenFormProps = {
|
||||
// 默认展开
|
||||
collapsed: false,
|
||||
@@ -15,6 +19,20 @@ export const formOptions: VbenFormProps = {
|
||||
fieldName: 'search_time',
|
||||
label: '时间范围',
|
||||
},
|
||||
{
|
||||
component: 'StorePicker',
|
||||
fieldName: 'store_id',
|
||||
label: '诊所',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'order_no',
|
||||
label: '订单号',
|
||||
componentProps: {
|
||||
placeholder: '请输入订单号',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
// 控制表单是否显示折叠按钮
|
||||
showCollapseButton: true,
|
||||
|
||||
@@ -18,6 +18,13 @@ export const gridOptions: VxeGridProps<RowType> = {
|
||||
columns: [
|
||||
{ field: 'id', align: 'left', title: 'ID' },
|
||||
{ field: 'store.name', title: '诊所' },
|
||||
// 订单号可点开详情,配合搜索筛选项提升「按单号查流水」体验
|
||||
{
|
||||
field: 'order_no',
|
||||
title: '订单号',
|
||||
minWidth: 160,
|
||||
slots: { default: 'order_no' },
|
||||
},
|
||||
{ field: 'doctor_info.name', title: '医生' },
|
||||
{ field: 'price', title: '金额' },
|
||||
{ field: 'order_type', title: '订单类型', slots: { default: 'order-type' } },
|
||||
|
||||
@@ -1,20 +1,40 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
/**
|
||||
* 资金流水记录页
|
||||
* Tab1:支付/提现类资金流水(xk_fund_water)
|
||||
* Tab2:账户资金变动全量审计(audit=1 + 诊所筛选),与提现页「只看本主体」区分
|
||||
*/
|
||||
import { nextTick, ref, watch } from 'vue';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { IconifyIcon as VbenIcon } from '@vben/icons';
|
||||
|
||||
import { Button, Tag } from 'ant-design-vue';
|
||||
import { Button, Tabs, Tag } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { useFreshSearchTime } from '#/composables/use-fresh-search-time';
|
||||
import { TableAction } from '#/components/table-action';
|
||||
import { useFreshSearchTime } from '#/composables/use-fresh-search-time';
|
||||
import { monthToTodayRangeDayjs } from '#/utils/search-time-range';
|
||||
import DetailModal from '#/views/business/order/product-order/components/detail.vue';
|
||||
import StatisticsReconciliation from '#/views/finance/reconciliation/components/statistics.vue';
|
||||
import ApplyOrdersModal from '#/views/finance/withdrawal/components/apply-orders-modal.vue';
|
||||
import { useWithdrawOrderDetailModals } from '#/views/finance/withdrawal/utils/use-withdraw-order-detail';
|
||||
|
||||
import { accountChangeFormOptions } from './config/account-change-search';
|
||||
import { accountChangeGridOptions } from './config/account-change-table';
|
||||
import { formOptions } from './config/search';
|
||||
import { gridOptions } from './config/table';
|
||||
|
||||
defineOptions({ name: 'FinanceFundWater' });
|
||||
|
||||
/** 记住 Tab,刷新后仍停在上次查看的列表 */
|
||||
const TAB_STORAGE_KEY = 'finance_fund_water_tab';
|
||||
const TAB_FUND = 'fundWater';
|
||||
const TAB_ABLE = 'accountChange';
|
||||
const allowedTabs = new Set([TAB_FUND, TAB_ABLE]);
|
||||
const storedTab = localStorage.getItem(TAB_STORAGE_KEY) || TAB_FUND;
|
||||
const activeTab = ref(allowedTabs.has(storedTab) ? storedTab : TAB_FUND);
|
||||
|
||||
const freshSearchTimeBridge = {
|
||||
resetSearchTime: async () => {},
|
||||
markSearchTimeTouched: () => {},
|
||||
@@ -41,98 +61,226 @@ const freshSearchTime = useFreshSearchTime({
|
||||
onRefresh: () => gridApi.reload(),
|
||||
});
|
||||
freshSearchTimeBridge.resetSearchTime = freshSearchTime.resetSearchTime;
|
||||
freshSearchTimeBridge.markSearchTimeTouched = freshSearchTime.markSearchTimeTouched;
|
||||
freshSearchTimeBridge.markSearchTimeTouched =
|
||||
freshSearchTime.markSearchTimeTouched;
|
||||
|
||||
/** 资金变动列表:平台全量(audit=1),可按诊所筛选 */
|
||||
const [AccountChangeGrid, accountChangeGridApi] = useVbenVxeGrid({
|
||||
formOptions: accountChangeFormOptions,
|
||||
gridOptions: accountChangeGridOptions,
|
||||
});
|
||||
|
||||
const statistics = ref();
|
||||
const applyOrdersModalRef = ref<InstanceType<typeof ApplyOrdersModal>>();
|
||||
|
||||
const [OrderDetailModal, OrderDetailModalApi] = useVbenModal({
|
||||
const { OrderDetailModal, RegisterDetailModal, TraceDrawer, openOrderDetail } =
|
||||
useWithdrawOrderDetailModals();
|
||||
|
||||
/** 资金流水行:仅商品订单详情(流水表本身偏产品单) */
|
||||
const [FundOrderDetailModal, fundOrderDetailModalApi] = useVbenModal({
|
||||
connectedComponent: DetailModal,
|
||||
});
|
||||
|
||||
const infoModal = (id) => {
|
||||
OrderDetailModalApi.setData({
|
||||
// 表单值
|
||||
id,
|
||||
});
|
||||
OrderDetailModalApi.open();
|
||||
const infoModal = (id: number) => {
|
||||
if (!id) return;
|
||||
fundOrderDetailModalApi.setData({ id });
|
||||
fundOrderDetailModalApi.open();
|
||||
};
|
||||
|
||||
/** 提现类变动可能挂多笔订单,先列表再下钻 */
|
||||
function showApplyOrders(row: Record<string, any>) {
|
||||
const orders = row.orders || [];
|
||||
if (orders.length === 1) {
|
||||
openOrderDetail(orders[0]);
|
||||
return;
|
||||
}
|
||||
applyOrdersModalRef.value?.open({
|
||||
title: row.order_no ? `关联订单 · ${row.order_no}` : '关联订单',
|
||||
orders,
|
||||
});
|
||||
}
|
||||
|
||||
/** 资金变动「订单详情」:单笔直开,提现多笔走关联列表 */
|
||||
function onAccountChangeOrder(row: Record<string, any>) {
|
||||
const orders = row.orders || [];
|
||||
if (
|
||||
orders.length > 1 ||
|
||||
(row.source_type === 'withdraw' && orders.length > 0)
|
||||
) {
|
||||
showApplyOrders(row);
|
||||
return;
|
||||
}
|
||||
openOrderDetail(row);
|
||||
}
|
||||
|
||||
/** 切换 Tab 并持久化;切回后重算表格高度,避免缩成一行 */
|
||||
function handleTabChange(key: string | number) {
|
||||
const next = String(key);
|
||||
if (!allowedTabs.has(next)) return;
|
||||
activeTab.value = next;
|
||||
localStorage.setItem(TAB_STORAGE_KEY, next);
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
gridApi.grid?.recalculate?.();
|
||||
accountChangeGridApi.grid?.recalculate?.();
|
||||
}, 50);
|
||||
});
|
||||
}
|
||||
|
||||
watch(activeTab, (key) => {
|
||||
if (key === TAB_ABLE) {
|
||||
nextTick(() => accountChangeGridApi.grid?.recalculate?.());
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height title="资金流水记录">
|
||||
<FundOrderDetailModal />
|
||||
<OrderDetailModal />
|
||||
<RegisterDetailModal />
|
||||
<TraceDrawer />
|
||||
<ApplyOrdersModal ref="applyOrdersModalRef" @view-order="openOrderDetail" />
|
||||
<StatisticsReconciliation v-if="statistics" :statistics="statistics" />
|
||||
<Grid>
|
||||
<template #toolbar-actions>
|
||||
<TableAction :actions="[]" :drop-down-actions="[]">
|
||||
<template #more>
|
||||
<Button style="margin-left: 16px">
|
||||
批量操作
|
||||
<Icon icon="ant-design:down-outlined" />
|
||||
</Button>
|
||||
<Tabs
|
||||
:active-key="activeTab"
|
||||
class="fund-water-tabs"
|
||||
type="card"
|
||||
:destroy-inactive-tab-pane="false"
|
||||
@change="handleTabChange"
|
||||
>
|
||||
<Tabs.TabPane :key="TAB_FUND" tab="资金流水">
|
||||
<Grid>
|
||||
<template #toolbar-actions>
|
||||
<TableAction :actions="[]" :drop-down-actions="[]" />
|
||||
</template>
|
||||
</TableAction>
|
||||
</template>
|
||||
<template #order-type="{ row }">
|
||||
<div class="mt-3">
|
||||
<Tag v-if="row.order_type === 1" color="purple">产品订单</Tag>
|
||||
<Tag v-else-if="row.order_type === 2" color="green">挂号订单</Tag>
|
||||
<Tag v-else-if="row.order_type === 3" color="blue">问诊订单</Tag>
|
||||
</div>
|
||||
</template>
|
||||
<template #type="{ row }">
|
||||
<div class="mt-3">
|
||||
<Tag v-if="row.type === 'refund'" color="purple">出账</Tag>
|
||||
<Tag v-else-if="row.type === 'enter'" color="green">入账</Tag>
|
||||
<Tag v-else-if="row.type === 'withdraw'" color="orange">提现</Tag>
|
||||
<Tag v-else-if="row.type === 'charge'" color="red">代付手续费</Tag>
|
||||
</div>
|
||||
</template>
|
||||
<template #toolbar-tools></template>
|
||||
<template #action="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '详情',
|
||||
type: 'link',
|
||||
icon: 'marketeq:eye',
|
||||
size: 'small',
|
||||
// auth: ['order', 'sys:role:detail'],
|
||||
onClick: infoModal.bind(null, row.order_id),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
<!-- 订单号链接:与操作列「详情」同一套 infoModal -->
|
||||
<template #order_no="{ row }">
|
||||
<Button
|
||||
v-if="row.order_no"
|
||||
type="link"
|
||||
size="small"
|
||||
@click="infoModal(row.order_id)"
|
||||
>
|
||||
{{ row.order_no }}
|
||||
</Button>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
<template #order-type="{ row }">
|
||||
<div class="mt-3">
|
||||
<Tag v-if="row.order_type === 1" color="purple">产品订单</Tag>
|
||||
<Tag v-else-if="row.order_type === 2" color="green">挂号订单</Tag>
|
||||
<Tag v-else-if="row.order_type === 3" color="blue">问诊订单</Tag>
|
||||
</div>
|
||||
</template>
|
||||
<template #type="{ row }">
|
||||
<div class="mt-3">
|
||||
<Tag v-if="row.type === 'refund'" color="purple">出账</Tag>
|
||||
<Tag v-else-if="row.type === 'enter'" color="green">入账</Tag>
|
||||
<Tag v-else-if="row.type === 'withdraw'" color="orange">提现</Tag>
|
||||
<Tag v-else-if="row.type === 'charge'" color="red">代付手续费</Tag>
|
||||
</div>
|
||||
</template>
|
||||
<template #toolbar-tools></template>
|
||||
<template #action="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '详情',
|
||||
type: 'link',
|
||||
icon: 'marketeq:eye',
|
||||
size: 'small',
|
||||
onClick: infoModal.bind(null, row.order_id),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane :key="TAB_ABLE" tab="资金变动记录">
|
||||
<!-- 全量 able_change:含分账结算、历史补入、退款、提现等;非本主体视角 -->
|
||||
<AccountChangeGrid>
|
||||
<template #toolbar-actions></template>
|
||||
<template #toolbar-tools></template>
|
||||
<template #store_name="{ row }">
|
||||
<span v-if="Number(row.user_type) === 1">
|
||||
{{ row.store_name || `门店#${row.user_id}` }}
|
||||
</span>
|
||||
<span v-else class="cell-muted">
|
||||
{{ row.user_type_txt || '-' }}
|
||||
<template v-if="row.user_id"> · #{{ row.user_id }}</template>
|
||||
</span>
|
||||
</template>
|
||||
<template #change_amount="{ row }">
|
||||
<span
|
||||
v-if="Number(row.change_amount) > 0"
|
||||
class="amount-cell is-up"
|
||||
>
|
||||
<VbenIcon icon="lucide:trending-up" class="amount-icon" />
|
||||
+{{ Number(row.change_amount).toFixed(2) }}
|
||||
</span>
|
||||
<span
|
||||
v-else-if="Number(row.change_amount) < 0"
|
||||
class="amount-cell is-down"
|
||||
>
|
||||
<VbenIcon icon="lucide:trending-down" class="amount-icon" />
|
||||
{{ Number(row.change_amount).toFixed(2) }}
|
||||
</span>
|
||||
<span v-else class="cell-muted">0.00</span>
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '订单详情',
|
||||
type: 'link',
|
||||
size: 'small',
|
||||
ifShow:
|
||||
!!row.order_id || (row.orders && row.orders.length > 0),
|
||||
onClick: onAccountChangeOrder.bind(null, row),
|
||||
},
|
||||
]"
|
||||
:drop-down-actions="[]"
|
||||
/>
|
||||
</template>
|
||||
</AccountChangeGrid>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.custom-list {
|
||||
list-style-type: none;
|
||||
padding-left: 0;
|
||||
.fund-water-tabs {
|
||||
:deep(.ant-tabs-content),
|
||||
:deep(.ant-tabs-tabpane) {
|
||||
min-height: 480px;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-list-item {
|
||||
background-color: rgba(64, 158, 255, 0.04);
|
||||
border-radius: 4px;
|
||||
margin-bottom: 8px;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
transition: background-color 0.3s;
|
||||
/* 资金变动金额:涨绿跌红走主题变量,适配暗色 */
|
||||
.amount-cell {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-family: 'DIN Alternate', ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(64, 158, 255, 0.1);
|
||||
&.is-up {
|
||||
color: hsl(var(--success));
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background-color: #409eff;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
vertical-align: middle;
|
||||
&.is-down {
|
||||
color: hsl(var(--destructive));
|
||||
}
|
||||
}
|
||||
|
||||
.amount-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.cell-muted {
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -14,13 +14,16 @@ import { clearPublicAccountLock } from '#/utils/publicAccountLock';
|
||||
|
||||
import {
|
||||
getPublicAccountDailyBillInfo,
|
||||
getPublicAccountDailyMergeDetail,
|
||||
uploadPublicAccountDailyBill,
|
||||
} from '../api';
|
||||
import {
|
||||
buildDailyUploadFormData,
|
||||
collectDailyPaymentVouchers,
|
||||
formatDailyPlatformAmount,
|
||||
normalizeDailyVoucherUrls,
|
||||
resolveDailyBillDateTxt,
|
||||
resolveDailyMergeId,
|
||||
resolveDailyPlatformAmount,
|
||||
resolveDailyStoreName,
|
||||
resolveDailyUploadId,
|
||||
@@ -100,14 +103,35 @@ async function load(data?: DailyUploadFormData) {
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
const detail = await getPublicAccountDailyBillInfo(dailyId.value);
|
||||
existVouchers.value = detail?.vouchers || detail?.payment_vouchers || [];
|
||||
storeName.value = resolveDailyStoreName(detail) || storeName.value;
|
||||
if (!presetAmount || presetAmount === '0.00') {
|
||||
platformAmount.value = resolveDailyPlatformAmount(detail);
|
||||
}
|
||||
if (!presetDate) {
|
||||
billDateTxt.value = resolveDailyBillDateTxt(detail);
|
||||
const mergeId = resolveDailyMergeId(normalized);
|
||||
// 周期行必须打 merge 详情拿合计金额,单日 daily-detail 可能是 0
|
||||
if (mergeId > 0) {
|
||||
const merge = await getPublicAccountDailyMergeDetail(mergeId);
|
||||
existVouchers.value = collectDailyPaymentVouchers(merge?.items || []);
|
||||
storeName.value =
|
||||
resolveDailyStoreName(merge?.items?.[0]) || storeName.value;
|
||||
if (!presetAmount || presetAmount === '0.00') {
|
||||
platformAmount.value = resolveDailyPlatformAmount({
|
||||
...merge,
|
||||
is_period: 1,
|
||||
platform_amount_total: merge?.platform_amount_total,
|
||||
platform_amount: merge?.platform_amount_total,
|
||||
items: merge?.items,
|
||||
});
|
||||
}
|
||||
if (!presetDate) {
|
||||
billDateTxt.value = resolveDailyBillDateTxt(merge);
|
||||
}
|
||||
} else {
|
||||
const detail = await getPublicAccountDailyBillInfo(dailyId.value);
|
||||
existVouchers.value = detail?.vouchers || detail?.payment_vouchers || [];
|
||||
storeName.value = resolveDailyStoreName(detail) || storeName.value;
|
||||
if (!presetAmount || presetAmount === '0.00') {
|
||||
platformAmount.value = resolveDailyPlatformAmount(detail);
|
||||
}
|
||||
if (!presetDate) {
|
||||
billDateTxt.value = resolveDailyBillDateTxt(detail);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
|
||||
@@ -11,6 +11,20 @@ import DailyUploadForm from './daily-upload-form.vue';
|
||||
|
||||
const formRef = ref<InstanceType<typeof DailyUploadForm>>();
|
||||
|
||||
/**
|
||||
* connectedComponent 首开时子组件可能尚未挂上 ref,一次 nextTick 不够
|
||||
* 多等几帧再 load,避免应付款停在 0.00 且不打详情接口
|
||||
*/
|
||||
async function waitFormReady(maxTries = 12) {
|
||||
for (let i = 0; i < maxTries; i++) {
|
||||
if (formRef.value?.load) {
|
||||
return formRef.value;
|
||||
}
|
||||
await nextTick();
|
||||
}
|
||||
return formRef.value;
|
||||
}
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
fullscreenButton: false,
|
||||
draggable: true,
|
||||
@@ -32,10 +46,9 @@ const [Modal, modalApi] = useVbenModal({
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) return;
|
||||
await nextTick();
|
||||
await formRef.value?.load(
|
||||
buildDailyUploadFormData(modalApi.getData<Record<string, any>>() || {}),
|
||||
);
|
||||
const form = await waitFormReady();
|
||||
const row = modalApi.getData<Record<string, any>>() || {};
|
||||
await form?.load(buildDailyUploadFormData(row));
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -12,7 +12,13 @@ export interface DailyUploadRowLike {
|
||||
platform_amount_total?: number | string;
|
||||
bill_date_txt?: string;
|
||||
bill_date_range_txt?: string;
|
||||
items?: Array<{ id?: number | string; platform_amount?: number | string }>;
|
||||
items?: Array<{
|
||||
id?: number | string;
|
||||
platform_amount?: number | string;
|
||||
store_name?: string;
|
||||
vouchers?: Array<{ file_url?: string }>;
|
||||
payment_vouchers?: Array<{ file_url?: string }>;
|
||||
}>;
|
||||
}
|
||||
|
||||
/** 上传接口需要的真实日账单 id(周期行 id 常为 0) */
|
||||
@@ -80,16 +86,60 @@ export function resolveDailyBillDateTxt(
|
||||
return String(row.bill_date_range_txt || row.bill_date_txt || '').trim();
|
||||
}
|
||||
|
||||
/** 周期账单主键:列表行 id 常为 0,业务 id 在 merge_id */
|
||||
export function resolveDailyMergeId(row?: DailyUploadRowLike | null): number {
|
||||
if (!row) {
|
||||
return 0;
|
||||
}
|
||||
const mergeId = Number(row.merge_id || 0);
|
||||
if (mergeId > 0) {
|
||||
return mergeId;
|
||||
}
|
||||
if (Number(row.is_period || 0) === 1) {
|
||||
return Number(row.id || 0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** 汇总组内付款凭证(去重),周期详情走 merge 接口时用 */
|
||||
export function collectDailyPaymentVouchers(
|
||||
days?: Array<{
|
||||
vouchers?: Array<{ file_url?: string }>;
|
||||
payment_vouchers?: Array<{ file_url?: string }>;
|
||||
}>,
|
||||
): { file_url: string }[] {
|
||||
const seen: Record<string, boolean> = {};
|
||||
const list: { file_url: string }[] = [];
|
||||
for (const day of days || []) {
|
||||
const vouchers = day.payment_vouchers || day.vouchers || [];
|
||||
for (const v of vouchers) {
|
||||
const url = String(v?.file_url || '').trim();
|
||||
if (!url || seen[url]) {
|
||||
continue;
|
||||
}
|
||||
seen[url] = true;
|
||||
list.push({ file_url: url });
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/** 打开上传弹窗/表单时灌入的标准数据结构 */
|
||||
export function buildDailyUploadFormData(
|
||||
row: DailyUploadRowLike,
|
||||
extras?: Record<string, any>,
|
||||
) {
|
||||
const uploadId = resolveDailyUploadId(row);
|
||||
const mergeId = resolveDailyMergeId(row);
|
||||
return {
|
||||
...row,
|
||||
...extras,
|
||||
id: uploadId,
|
||||
merge_id: mergeId > 0 ? mergeId : Number(row.merge_id || 0),
|
||||
is_period:
|
||||
Number(row.is_period || 0) === 1 || mergeId > 0
|
||||
? 1
|
||||
: Number(row.is_period || 0),
|
||||
store_name: resolveDailyStoreName(row),
|
||||
platform_amount: resolveDailyPlatformAmount(row),
|
||||
bill_date_txt: resolveDailyBillDateTxt(row),
|
||||
|
||||
@@ -64,3 +64,8 @@ export async function resetPassword(id: number) {
|
||||
export async function getWithdrawableOrders(data: Record<string, any>) {
|
||||
return requestClient.get<any>(`${prefix}withdrawable-orders`, { params: data });
|
||||
}
|
||||
|
||||
/** 提现规则文案 xk-api GET /withdrawal-application/rules(系统配置) */
|
||||
export async function getWithdrawalRules() {
|
||||
return requestClient.get<any>(`${prefix}rules`);
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ const emit = defineEmits<{
|
||||
apply: [];
|
||||
editCard: [];
|
||||
refresh: [];
|
||||
rules: [];
|
||||
statClick: [key: string];
|
||||
viewApply: [id: number];
|
||||
}>();
|
||||
@@ -212,6 +213,7 @@ function formatMoney(val: number) {
|
||||
{{ subjectName }} · 账户信息
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<Button size="small" @click="emit('rules')">提现规则</Button>
|
||||
<Button type="primary" size="small" @click="emit('apply')">
|
||||
申请提现
|
||||
</Button>
|
||||
|
||||
@@ -15,6 +15,7 @@ import AccountInfoCard from './account-info-card.vue';
|
||||
import SaveMyCard from './card-modal.vue';
|
||||
import GapConfirmModal from './gap-confirm-modal.vue';
|
||||
import FormModalDemo from './withdrawal-modal.vue';
|
||||
import WithdrawalRulesModal from './withdrawal-rules-modal.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'Statistics',
|
||||
@@ -97,6 +98,9 @@ const [FormModal, formModalApi] = useVbenModal({
|
||||
const [saveMyCardModal, saveMyCardApi] = useVbenModal({
|
||||
connectedComponent: SaveMyCard,
|
||||
});
|
||||
const [RulesModal, rulesModalApi] = useVbenModal({
|
||||
connectedComponent: WithdrawalRulesModal,
|
||||
});
|
||||
/** 差额确认与申请弹窗同级挂载;zIndex 内外都写,避免 connectedComponent 吃掉内部层级 */
|
||||
const [GapConfirmModalComp, gapConfirmModalApi] = useVbenModal({
|
||||
connectedComponent: GapConfirmModal,
|
||||
@@ -143,6 +147,11 @@ const showCardModal = () => {
|
||||
});
|
||||
saveMyCardApi.open();
|
||||
};
|
||||
|
||||
/** 打开提现规则说明(额度 / 次数 / 到账 / 审核流程) */
|
||||
function showRulesModal() {
|
||||
rulesModalApi.open();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -150,6 +159,7 @@ const showCardModal = () => {
|
||||
<FormModal />
|
||||
<GapConfirmModalComp />
|
||||
<saveMyCardModal />
|
||||
<RulesModal />
|
||||
<AccountInfoCard
|
||||
:my-card="myCard"
|
||||
:is-store-user="isStoreUser"
|
||||
@@ -170,6 +180,7 @@ const showCardModal = () => {
|
||||
@apply="showModal"
|
||||
@edit-card="showCardModal"
|
||||
@refresh="refresh"
|
||||
@rules="showRulesModal"
|
||||
@stat-click="emit('statClick', $event)"
|
||||
@view-apply="emit('viewApply', $event)"
|
||||
/>
|
||||
|
||||
@@ -157,7 +157,7 @@ function dakuanStatusPill(status: number) {
|
||||
<template>
|
||||
<Page
|
||||
auto-content-height
|
||||
description="提现前请确认好您的打款账户"
|
||||
description="提现前请确认打款账户,并可查看提现规则"
|
||||
title="提现管理"
|
||||
>
|
||||
<OrderDetailModal />
|
||||
|
||||
@@ -18,6 +18,13 @@ export async function getOldApiLogList(data: any) {
|
||||
return requestClient.get<any>(`${prefix}api-list`, { params: data });
|
||||
}
|
||||
|
||||
/**
|
||||
* 单条日志详情。列表不返回 param/result,查看时再取,避免大 JSON 拖慢分页
|
||||
*/
|
||||
export async function getApiOpLogDetail(id: number) {
|
||||
return requestClient.get<any>(`${prefix}api-detail`, { params: { id } });
|
||||
}
|
||||
|
||||
/**
|
||||
* Api 访问日志操作用户搜索
|
||||
*/
|
||||
|
||||
@@ -3,19 +3,30 @@ import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Descriptions } from 'ant-design-vue';
|
||||
import { Descriptions, Spin } from 'ant-design-vue';
|
||||
|
||||
import { Icon } from '#/components/icon';
|
||||
import { getIcon } from '#/util/tool';
|
||||
import { getApiOpLogDetail } from '../api';
|
||||
import {
|
||||
getApiOpLogOperatorLabel,
|
||||
getApiOpLogPlatformLabel,
|
||||
} from '../config/platform';
|
||||
|
||||
|
||||
|
||||
const gridApi = ref();
|
||||
const data = ref({});
|
||||
const data = ref<Record<string, any>>({});
|
||||
const loading = ref(false);
|
||||
|
||||
/** 列表行没有 param/result,打开时按 id 拉详情;解析失败则原样展示,避免弹窗白屏 */
|
||||
function formatJson(raw: unknown) {
|
||||
if (raw == null || raw === '') return '';
|
||||
if (typeof raw === 'object') return raw;
|
||||
try {
|
||||
return JSON.parse(String(raw));
|
||||
} catch {
|
||||
return raw;
|
||||
}
|
||||
}
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
fullscreenButton: false,
|
||||
@@ -26,13 +37,20 @@ const [Modal, modalApi] = useVbenModal({
|
||||
onConfirm: async () => {
|
||||
modalApi.close();
|
||||
},
|
||||
onOpenChange(isOpen: boolean) {
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
|
||||
if (isOpen) {
|
||||
const { values } = modalApi.getData<Record<string, any>>();
|
||||
if (values) {
|
||||
data.value = values;
|
||||
}
|
||||
if (!isOpen) return;
|
||||
const { values } = modalApi.getData<Record<string, any>>();
|
||||
data.value = values || {};
|
||||
const id = Number(values?.id || 0);
|
||||
if (!id) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
data.value = (await getApiOpLogDetail(id)) || values || {};
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -40,6 +58,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
|
||||
<template>
|
||||
<Modal class="w-[80%] h-[80%]" title="Api访问日志详情">
|
||||
<Spin :spinning="loading">
|
||||
<div class="flex flex-col gap-4">
|
||||
<Descriptions
|
||||
:column="{ xxl: 2, xl: 2, lg: 2, md: 2, sm: 2, xs: 2 }"
|
||||
@@ -91,13 +110,14 @@ const [Modal, modalApi] = useVbenModal({
|
||||
bordered
|
||||
>
|
||||
<Descriptions.Item label="参数">
|
||||
<pre>{{ JSON.parse(data.param) }}</pre>
|
||||
<pre>{{ formatJson(data.param) }}</pre>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="结果">
|
||||
<pre>{{ JSON.parse(data.result) }}</pre>
|
||||
<pre>{{ formatJson(data.result) }}</pre>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</div>
|
||||
</Spin>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -7,11 +7,29 @@ import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Button, Descriptions, message, Spin, Tag } from 'ant-design-vue';
|
||||
import { Button, Descriptions, Image, message, Spin, Tabs, Tag } from 'ant-design-vue';
|
||||
|
||||
import { getAiGenerationDetail } from '../api';
|
||||
import GenerationFlowTimeline from './generation-flow-timeline.vue';
|
||||
|
||||
const TabPane = Tabs.TabPane;
|
||||
|
||||
/** 详情内容区 Tab:preview=结构化预览 / json=原始快照;本地记忆 */
|
||||
const CONTENT_TAB_STORAGE_KEY = 'ai_generation_detail_content_tab';
|
||||
|
||||
function readContentTab(): 'preview' | 'json' {
|
||||
const v = localStorage.getItem(CONTENT_TAB_STORAGE_KEY);
|
||||
return v === 'json' ? 'json' : 'preview';
|
||||
}
|
||||
|
||||
const contentTab = ref<'preview' | 'json'>(readContentTab());
|
||||
|
||||
function onContentTabChange(key: string | number) {
|
||||
const next = String(key) === 'json' ? 'json' : 'preview';
|
||||
contentTab.value = next;
|
||||
localStorage.setItem(CONTENT_TAB_STORAGE_KEY, next);
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
/** 打开页面级归档弹窗 */
|
||||
archive: [row: Record<string, any>];
|
||||
@@ -22,6 +40,73 @@ const emit = defineEmits<{
|
||||
const loading = ref(false);
|
||||
const detail = ref<Record<string, any>>({});
|
||||
|
||||
/**
|
||||
* 识图场景:input_snapshot.image_urls 是上传的处方原图
|
||||
* 详情里除 JSON 外单独渲染,避免只能看 URL 字符串
|
||||
*/
|
||||
const ocrImageUrls = computed(() => {
|
||||
const snap = detail.value?.input_snapshot;
|
||||
const urls = snap?.image_urls;
|
||||
if (!Array.isArray(urls)) return [] as string[];
|
||||
return urls
|
||||
.map((u) => String(u || '').trim())
|
||||
.filter((u) => /^https?:\/\//i.test(u));
|
||||
});
|
||||
|
||||
const isOcrScene = computed(() => {
|
||||
const scene = String(detail.value?.scene || '');
|
||||
return (
|
||||
scene === 'prescription_image_ocr' || scene === 'transfer_image_ocr'
|
||||
);
|
||||
});
|
||||
|
||||
/** 识图结果结构化预览字段 */
|
||||
const ocrResult = computed(() => {
|
||||
const rj = detail.value?.result_json || {};
|
||||
const ocr = rj.ocr && typeof rj.ocr === 'object' ? rj.ocr : {};
|
||||
const match = rj.match && typeof rj.match === 'object' ? rj.match : {};
|
||||
const patientMatch =
|
||||
rj.patient_match && typeof rj.patient_match === 'object'
|
||||
? rj.patient_match
|
||||
: null;
|
||||
return {
|
||||
mode: String(rj.mode || ocr.mode || ''),
|
||||
patient_name: String(ocr.patient_name || ''),
|
||||
patient_mobile: String(ocr.patient_mobile || ''),
|
||||
sex: Number(ocr.sex || 0),
|
||||
age: Number(ocr.age || 0),
|
||||
patient_address: String(ocr.patient_address || ''),
|
||||
clinical_diagnose: String(
|
||||
rj.clinical_diagnose || ocr.clinical_diagnose || '',
|
||||
),
|
||||
medical_advice: String(rj.medical_advice || ocr.medical_advice || ''),
|
||||
dosage: Number(rj.dosage || ocr.dosage || 0),
|
||||
day_dosage: Number(rj.day_dosage || ocr.day_dosage || 0),
|
||||
matched: Array.isArray(match.matched) ? match.matched : [],
|
||||
unmatched: Array.isArray(match.unmatched) ? match.unmatched : [],
|
||||
patient_match: patientMatch,
|
||||
};
|
||||
});
|
||||
|
||||
function sexText(sex: number) {
|
||||
if (sex === 1) return '男';
|
||||
if (sex === 2) return '女';
|
||||
return '未知';
|
||||
}
|
||||
|
||||
function candidateDrugName(c: Record<string, any>) {
|
||||
return String(
|
||||
c?.drug_name || c?.drug?.drug_name || c?.matched_alias || c?.name || '',
|
||||
).trim();
|
||||
}
|
||||
|
||||
function selectedCandidateName(row: Record<string, any>) {
|
||||
const sid = Number(row?.selected_drug_id || 0);
|
||||
const list = Array.isArray(row?.candidates) ? row.candidates : [];
|
||||
const hit = list.find((c: any) => Number(c?.drug_id) === sid) || list[0];
|
||||
return hit ? candidateDrugName(hit) : '';
|
||||
}
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
title: '生成记录详情',
|
||||
class: 'w-[min(1280px,96vw)]',
|
||||
@@ -345,19 +430,204 @@ function onArchiveClick() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="briefPreview" class="json-panel mt-3">
|
||||
<div v-if="briefPreview && !isOcrScene" class="json-panel mt-3">
|
||||
<div class="json-title">简报正文</div>
|
||||
<pre class="json-pre">{{ briefPreview }}</pre>
|
||||
</div>
|
||||
<div class="mt-3 grid grid-cols-1 gap-3 lg:grid-cols-2">
|
||||
<div class="json-panel">
|
||||
<div class="json-title">输入快照</div>
|
||||
<pre class="json-pre">{{ prettyJson(detail.input_snapshot) }}</pre>
|
||||
<!-- 识图开方/传方:原图始终展示,便于对照下方预览/JSON -->
|
||||
<div v-if="isOcrScene" class="json-panel mt-3">
|
||||
<div class="json-title">
|
||||
处方原图
|
||||
<span class="ml-2 text-[hsl(var(--muted-foreground))]">
|
||||
{{ ocrImageUrls.length ? `${ocrImageUrls.length} 张` : '无图' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="json-panel">
|
||||
<div class="json-title">结果 JSON</div>
|
||||
<pre class="json-pre">{{ prettyJson(detail.result_json) }}</pre>
|
||||
<div v-if="ocrImageUrls.length" class="ocr-img-row">
|
||||
<Image.PreviewGroup>
|
||||
<Image
|
||||
v-for="(url, idx) in ocrImageUrls"
|
||||
:key="`${url}-${idx}`"
|
||||
:src="url"
|
||||
:width="120"
|
||||
:height="120"
|
||||
class="ocr-thumb"
|
||||
:preview="{ src: url }"
|
||||
/>
|
||||
</Image.PreviewGroup>
|
||||
</div>
|
||||
<div v-else class="ocr-img-empty">
|
||||
本条记录未写入 image_urls(请确认识图时已成功上传)
|
||||
</div>
|
||||
</div>
|
||||
<!-- 生成内容:详情预览 / JSON,localStorage 记忆 -->
|
||||
<div class="mt-3">
|
||||
<Tabs
|
||||
:active-key="contentTab"
|
||||
size="small"
|
||||
@change="onContentTabChange"
|
||||
>
|
||||
<TabPane key="preview" tab="详情预览">
|
||||
<div v-if="isOcrScene" class="preview-body">
|
||||
<Descriptions
|
||||
size="small"
|
||||
bordered
|
||||
:column="2"
|
||||
class="preview-desc"
|
||||
>
|
||||
<Descriptions.Item label="模式">
|
||||
{{
|
||||
ocrResult.mode === 'transfer' ? '识图传方' : '识图开方'
|
||||
}}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item
|
||||
v-if="ocrResult.patient_match"
|
||||
label="患者匹配"
|
||||
>
|
||||
<Tag
|
||||
:color="
|
||||
ocrResult.patient_match.matched ? 'success' : 'warning'
|
||||
"
|
||||
>
|
||||
{{
|
||||
ocrResult.patient_match.matched
|
||||
? `已匹配 #${ocrResult.patient_match.user_patient_id || ''}`
|
||||
: '未匹配(将新建)'
|
||||
}}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item
|
||||
v-if="ocrResult.patient_name || ocrResult.mode === 'transfer'"
|
||||
label="姓名"
|
||||
>
|
||||
{{ ocrResult.patient_name || '—' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item
|
||||
v-if="
|
||||
ocrResult.patient_mobile || ocrResult.mode === 'transfer'
|
||||
"
|
||||
label="手机"
|
||||
>
|
||||
{{ ocrResult.patient_mobile || '—' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item
|
||||
v-if="ocrResult.mode === 'transfer'"
|
||||
label="性别/年龄"
|
||||
>
|
||||
{{ sexText(ocrResult.sex) }} /
|
||||
{{ ocrResult.age > 0 ? `${ocrResult.age} 岁` : '—' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item
|
||||
v-if="ocrResult.patient_address"
|
||||
label="地址"
|
||||
:span="2"
|
||||
>
|
||||
{{ ocrResult.patient_address }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="诊断" :span="2">
|
||||
{{ ocrResult.clinical_diagnose || '—' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="医嘱" :span="2">
|
||||
{{ ocrResult.medical_advice || '—' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="剂数/日次">
|
||||
{{ ocrResult.dosage || '—' }} 剂 /
|
||||
{{ ocrResult.day_dosage || '—' }} 次
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="药味">
|
||||
已对照 {{ ocrResult.matched.length }} /
|
||||
未对照 {{ ocrResult.unmatched.length }}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
<div
|
||||
v-if="ocrResult.matched.length"
|
||||
class="preview-section mt-3"
|
||||
>
|
||||
<div class="preview-section-title">
|
||||
已对照({{ ocrResult.matched.length }})
|
||||
</div>
|
||||
<!-- 紧凑一行多列:AI名+克数 → 对照药名 -->
|
||||
<div class="preview-drug-grid">
|
||||
<div
|
||||
v-for="(m, mi) in ocrResult.matched"
|
||||
:key="'m' + mi"
|
||||
class="preview-drug-chip is-matched"
|
||||
:title="
|
||||
selectedCandidateName(m)
|
||||
? `${m.ai_name || ''} → ${selectedCandidateName(m)}`
|
||||
: m.ai_name || ''
|
||||
"
|
||||
>
|
||||
<span class="chip-ai">{{ m.ai_name || '—' }}</span>
|
||||
<span class="chip-dose">
|
||||
{{ m.dose || '' }}{{ m.unit || '' }}
|
||||
</span>
|
||||
<span
|
||||
v-if="selectedCandidateName(m)"
|
||||
class="chip-arrow"
|
||||
>→</span>
|
||||
<span
|
||||
v-if="selectedCandidateName(m)"
|
||||
class="chip-match"
|
||||
>{{ selectedCandidateName(m) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="ocrResult.unmatched.length"
|
||||
class="preview-section mt-3"
|
||||
>
|
||||
<div class="preview-section-title text-warn">
|
||||
未对照({{ ocrResult.unmatched.length }})
|
||||
</div>
|
||||
<div class="preview-drug-grid">
|
||||
<div
|
||||
v-for="(u, ui) in ocrResult.unmatched"
|
||||
:key="'u' + ui"
|
||||
class="preview-drug-chip is-unmatched"
|
||||
>
|
||||
<span class="chip-ai">{{ u.ai_name || '—' }}</span>
|
||||
<span class="chip-dose">
|
||||
{{ u.dose || '' }}{{ u.unit || '' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="
|
||||
!ocrResult.matched.length && !ocrResult.unmatched.length
|
||||
"
|
||||
class="ocr-img-empty"
|
||||
>
|
||||
暂无药品对照结果
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="briefPreview" class="preview-body">
|
||||
<pre class="json-pre preview-brief">{{ briefPreview }}</pre>
|
||||
</div>
|
||||
<div v-else class="ocr-img-empty">
|
||||
当前场景暂无结构化预览,请切换到「JSON」查看原始内容
|
||||
</div>
|
||||
</TabPane>
|
||||
<TabPane key="json" tab="JSON">
|
||||
<div class="grid grid-cols-1 gap-3 lg:grid-cols-2">
|
||||
<div class="json-panel">
|
||||
<div class="json-title">输入快照</div>
|
||||
<pre class="json-pre">{{
|
||||
prettyJson(detail.input_snapshot)
|
||||
}}</pre>
|
||||
</div>
|
||||
<div class="json-panel">
|
||||
<div class="json-title">结果 JSON</div>
|
||||
<pre class="json-pre">{{
|
||||
prettyJson(detail.result_json)
|
||||
}}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</Spin>
|
||||
@@ -406,6 +676,106 @@ function onArchiveClick() {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.ocr-img-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.ocr-thumb {
|
||||
overflow: hidden;
|
||||
object-fit: cover;
|
||||
cursor: pointer;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 0 6px hsl(var(--primary) / 18%);
|
||||
}
|
||||
|
||||
.ocr-img-empty {
|
||||
padding: 16px 12px;
|
||||
font-size: 13px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.preview-body {
|
||||
padding: 4px 0 8px;
|
||||
}
|
||||
|
||||
.preview-desc :deep(.ant-descriptions-item-label) {
|
||||
width: 96px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.preview-section-title {
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.preview-section-title.text-warn {
|
||||
color: hsl(var(--destructive, var(--warning)));
|
||||
}
|
||||
|
||||
.preview-drug-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.preview-drug-chip {
|
||||
display: inline-flex;
|
||||
flex-wrap: nowrap;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
padding: 3px 8px;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
background: hsl(var(--card, var(--background)));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.preview-drug-chip.is-matched {
|
||||
border-color: hsl(var(--primary) / 30%);
|
||||
box-shadow: 0 0 4px hsl(var(--primary) / 14%);
|
||||
}
|
||||
|
||||
.preview-drug-chip.is-unmatched {
|
||||
border-color: hsl(0 84% 60% / 40%);
|
||||
box-shadow: 0 0 4px hsl(0 84% 60% / 14%);
|
||||
}
|
||||
|
||||
.chip-ai {
|
||||
font-weight: 500;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.chip-dose {
|
||||
color: hsl(var(--muted-foreground));
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.chip-arrow {
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.chip-match {
|
||||
color: hsl(var(--primary));
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
font-size: 12px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.preview-brief {
|
||||
max-height: 360px;
|
||||
}
|
||||
|
||||
.kb-hits-panel {
|
||||
overflow: hidden;
|
||||
background: hsl(var(--card, var(--background)));
|
||||
|
||||
@@ -12,6 +12,8 @@ export const AI_GENERATION_SCENE_OPTIONS = [
|
||||
{ label: '出方', value: 'prescription' },
|
||||
{ label: '语音口述开方', value: 'voice_prescription' },
|
||||
{ label: '每日简报', value: 'daily_brief' },
|
||||
{ label: '识图开方', value: 'prescription_image_ocr' },
|
||||
{ label: '识图传方', value: 'transfer_image_ocr' },
|
||||
];
|
||||
|
||||
/** 是否命中业务缓存(简报按天缓存等) */
|
||||
|
||||
@@ -59,15 +59,15 @@ export const reportFormProps: VbenFormProps = {
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '对公账户', value: '1' },
|
||||
{ label: '法人账户', value: '2' },
|
||||
// { label: '对公账户', value: '1' },
|
||||
// { label: '法人账户', value: '2' },
|
||||
{ label: '授权对公账户', value: '3' },
|
||||
{ label: '授权个人账户', value: '4' },
|
||||
],
|
||||
},
|
||||
fieldName: 'bank_account_type',
|
||||
label: '账户类型',
|
||||
defaultValue: '2',
|
||||
defaultValue: '4',
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
|
||||
@@ -103,7 +103,7 @@ export const bankCardReportFormProps: VbenFormProps = {
|
||||
fieldName: 'account_type',
|
||||
label: '账户类型',
|
||||
rules: 'required',
|
||||
defaultValue: '2',
|
||||
defaultValue: 4,
|
||||
dependencies: {
|
||||
show: (values) => !values.only_remark,
|
||||
triggerFields: ['only_remark'],
|
||||
|
||||
@@ -274,6 +274,15 @@ export async function updateStorePublicAccountLockDays(data: {
|
||||
return requestClient.post<any>(`${prefix}update-public-account-lock-days`, data);
|
||||
}
|
||||
|
||||
/** 单独更新诊所订单分账类型与平台占比 */
|
||||
export async function updateStoreLedgerSplitApi(data: {
|
||||
id: number;
|
||||
ledger_split_type: number;
|
||||
ledger_split_percent: number;
|
||||
}) {
|
||||
return requestClient.post<any>(`${prefix}update-ledger-split`, data);
|
||||
}
|
||||
|
||||
export async function toggleSalespersonSeePriceApi(data: { store_id: number }) {
|
||||
return requestClient.post<any>('salesperson-store-config/toggle-see-price', data);
|
||||
}
|
||||
@@ -302,3 +311,18 @@ export async function updateStoreBankInfoApi(data: {
|
||||
}) {
|
||||
return requestClient.post<any>(`${prefix}update-store-bank-info`, data);
|
||||
}
|
||||
|
||||
/** 门店轮播图列表(平台端传 store_id) */
|
||||
export async function getStoreNavListApi(storeId: number) {
|
||||
return requestClient.get<any>(`${prefix}nav-list`, {
|
||||
params: { store_id: storeId },
|
||||
});
|
||||
}
|
||||
|
||||
/** 全量同步门店轮播图 URL(含排序) */
|
||||
export async function saveStoreNavUrlApi(data: {
|
||||
store_id: number;
|
||||
url: string[];
|
||||
}) {
|
||||
return requestClient.post<any>(`${prefix}upload-nav`, data);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
*/
|
||||
import { Switch, Tag } from 'ant-design-vue';
|
||||
|
||||
import { storeLedgerSplitTagText } from '../../config/constants';
|
||||
|
||||
defineProps<{
|
||||
row: Record<string, any>;
|
||||
section: 'switches' | 'business' | 'all';
|
||||
@@ -22,6 +24,8 @@ defineProps<{
|
||||
onPublicAccountPay?: (id: number) => void;
|
||||
/** 点击账期 Tag 打开配置弹窗 */
|
||||
onPublicAccountLockDays?: (row: Record<string, any>) => void;
|
||||
/** 点击订单分账 Tag 打开分账设置弹窗 */
|
||||
onLedgerSplit?: (row: Record<string, any>) => void;
|
||||
onBankCardReport: (row: Record<string, any>) => void;
|
||||
onClinicType?: (row: Record<string, any>) => void;
|
||||
onAllowInsurance?: (id: number) => void;
|
||||
@@ -121,6 +125,18 @@ function publicAccountLockDaysText(row: Record<string, any>): string {
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="onLedgerSplit" class="config-item">
|
||||
<span class="config-item__label">订单分账</span>
|
||||
<div class="config-item__control">
|
||||
<Tag
|
||||
:color="Number(row.ledger_split_type) === 1 ? 'processing' : 'default'"
|
||||
class="config-item__tag"
|
||||
@click="onLedgerSplit(row)"
|
||||
>
|
||||
{{ storeLedgerSplitTagText(row) }}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="section === 'business'">
|
||||
@@ -261,6 +277,18 @@ function publicAccountLockDaysText(row: Record<string, any>): string {
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="onLedgerSplit" class="config-item">
|
||||
<span class="config-item__label">订单分账</span>
|
||||
<div class="config-item__control">
|
||||
<Tag
|
||||
:color="Number(row.ledger_split_type) === 1 ? 'processing' : 'default'"
|
||||
class="config-item__tag"
|
||||
@click="onLedgerSplit(row)"
|
||||
>
|
||||
{{ storeLedgerSplitTagText(row) }}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { VbenFormProps } from '#/adapter/form';
|
||||
|
||||
import { STORE_LEDGER_SPLIT_TYPE_OPTIONS } from './constants';
|
||||
|
||||
// 诊所管理表单配置
|
||||
// 注意:已移除类型选择字段,固定为type=0(诊所类型)
|
||||
@@ -321,6 +322,38 @@ export const modalFormProps: VbenFormProps = {
|
||||
suffix: () => '%',
|
||||
defaultValue: 100,
|
||||
},
|
||||
{
|
||||
// 订单分账类型:默认正常;按比例时平台拿实付百分比(仅中药/颗粒)
|
||||
component: 'RadioGroup',
|
||||
formItemClass: 'col-span-6',
|
||||
componentProps: {
|
||||
options: STORE_LEDGER_SPLIT_TYPE_OPTIONS,
|
||||
},
|
||||
fieldName: 'ledger_split_type',
|
||||
label: '订单分账类型',
|
||||
defaultValue: 0,
|
||||
help: '按比例仅对中药/颗粒订单生效;西药等仍走正常分账',
|
||||
},
|
||||
{
|
||||
// 平台占实付 total_pay_price 的百分比,需管理员手动设置
|
||||
component: 'InputNumber',
|
||||
formItemClass: 'col-span-6',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
precision: 2,
|
||||
placeholder: '平台占实付百分比',
|
||||
},
|
||||
fieldName: 'ledger_split_percent',
|
||||
label: '平台分账占比',
|
||||
suffix: () => '%',
|
||||
defaultValue: 0,
|
||||
help: '平台占订单实付总额的百分比,其余归诊所;需手动调整',
|
||||
dependencies: {
|
||||
show: (values: Record<string, any>) => Number(values.ledger_split_type) === 1,
|
||||
triggerFields: ['ledger_split_type'],
|
||||
},
|
||||
},
|
||||
// {
|
||||
// component: 'VbenInput',
|
||||
// formItemClass: 'col-span-6',
|
||||
|
||||
@@ -59,6 +59,14 @@ export const gridOptions: VxeGridProps<RowType> = {
|
||||
width: 200,
|
||||
slots: { default: 'prescription_types_config' },
|
||||
},
|
||||
{
|
||||
// 订单分账独立列:点击 Tag 打开设置弹窗(与开关配置内入口一致)
|
||||
field: 'ledger_split_config',
|
||||
align: 'left',
|
||||
title: '订单分账',
|
||||
width: 140,
|
||||
slots: { default: 'ledger_split_config' },
|
||||
},
|
||||
{
|
||||
field: 'store_config_1',
|
||||
align: 'left',
|
||||
|
||||
@@ -42,7 +42,9 @@ import BindSpecialPrescriptionDoctorModal from './components/BindSpecialPrescrip
|
||||
import BindPrescriptionTypesModal from './components/BindPrescriptionTypesModal.vue';
|
||||
import StoreExternalFieldModal from './components/StoreExternalFieldModal.vue';
|
||||
import SalespersonCommissionDrawer from './components/SalespersonCommissionDrawer.vue';
|
||||
import LedgerSplitModal from './components/LedgerSplitModal.vue';
|
||||
import AddDoctorFromStoreModal from './components/AddDoctorFromStoreModal.vue';
|
||||
import StoreNavModal from './components/StoreNavModal.vue';
|
||||
import ScheduleCalendarModal from '#/views/doctor/doctor/components/schedule-calendar/ScheduleCalendarModal.vue';
|
||||
import StoreCardModal from '#/components/store-card/StoreCardModal.vue';
|
||||
import StoreVipCell from '#/components/vip/StoreVipCell.vue';
|
||||
@@ -54,6 +56,7 @@ import { getStoreBankCardReportDetail } from '#/views/system/store-bank-card/api
|
||||
import { formOptions } from './config/search';
|
||||
import { gridOptions } from './config/table';
|
||||
import { prescriptionTypeLabel } from './config/prescription-type-constants';
|
||||
import { storeLedgerSplitTagText } from './config/constants';
|
||||
import {
|
||||
canAddStoreDoctor,
|
||||
canManageStoreBankCard,
|
||||
@@ -205,6 +208,36 @@ const [BankCardStoreEditModalComponent, bankCardStoreEditModalApi] = useVbenModa
|
||||
connectedComponent: BankCardStoreEditModal,
|
||||
});
|
||||
|
||||
const [LedgerSplitModalComponent, ledgerSplitModalApi] = useVbenModal({
|
||||
connectedComponent: LedgerSplitModal,
|
||||
});
|
||||
|
||||
const [StoreNavModalComp, storeNavModalApi] = useVbenModal({
|
||||
connectedComponent: StoreNavModal,
|
||||
});
|
||||
|
||||
/**
|
||||
* 打开诊所订单分账设置弹窗(列表 Tag / 下拉入口共用)
|
||||
*/
|
||||
const openLedgerSplitModal = (row: Record<string, any>) => {
|
||||
if (!row?.id) return;
|
||||
ledgerSplitModalApi.setData({
|
||||
values: row,
|
||||
gridApi,
|
||||
});
|
||||
ledgerSplitModalApi.open();
|
||||
};
|
||||
|
||||
/** 打开门店轮播图配置(上传 / 删除 / 拖拽排序) */
|
||||
const openStoreNavModal = (row: Record<string, any>) => {
|
||||
if (!row?.id) return;
|
||||
storeNavModalApi.setData({
|
||||
storeId: row.id,
|
||||
storeName: row.name,
|
||||
});
|
||||
storeNavModalApi.open();
|
||||
};
|
||||
|
||||
const openBankCardStoreEdit = (row: any) => {
|
||||
bankCardStoreEditModalApi.setData({
|
||||
storeId: row.id,
|
||||
@@ -561,6 +594,8 @@ const handleSwitchClinicType = (row: any) => {
|
||||
<BankCardReportModalComponent />
|
||||
<BankCardStatusModalComponent />
|
||||
<BankCardStoreEditModalComponent />
|
||||
<LedgerSplitModalComponent />
|
||||
<StoreNavModalComp />
|
||||
<StoreCardModalComp />
|
||||
<VipHubModalComp />
|
||||
<AddDoctorModalComp />
|
||||
@@ -648,6 +683,7 @@ const handleSwitchClinicType = (row: any) => {
|
||||
:on-order-price-adjust="updateOrderPricePercentAdjust"
|
||||
:on-public-account-pay="updatePublicAccountPayEnabled"
|
||||
:on-public-account-lock-days="handlePublicAccountLockDays"
|
||||
:on-ledger-split="openLedgerSplitModal"
|
||||
:on-bank-card-report="handleBankCardReportColumnClick"
|
||||
/>
|
||||
</template>
|
||||
@@ -759,6 +795,13 @@ const handleSwitchClinicType = (row: any) => {
|
||||
</Tag>
|
||||
</div>
|
||||
</template>
|
||||
<template #ledger_split_config="{ row }">
|
||||
<div class="cursor-pointer" @click="openLedgerSplitModal(row)">
|
||||
<Tag :color="Number(row.ledger_split_type) === 1 ? 'processing' : 'default'">
|
||||
{{ storeLedgerSplitTagText(row) }}
|
||||
</Tag>
|
||||
</div>
|
||||
</template>
|
||||
<template #start-time="{ row }">
|
||||
早:<Tag color="success">{{ row.start_time }}</Tag>
|
||||
<br />
|
||||
@@ -777,6 +820,22 @@ const handleSwitchClinicType = (row: any) => {
|
||||
auth: ['Super Admin', 'Admin'],
|
||||
onClick: showModal.bind(null, row, true),
|
||||
},
|
||||
{
|
||||
label: '轮播图',
|
||||
type: 'link',
|
||||
icon: 'ant-design:picture-outlined',
|
||||
size: 'small',
|
||||
auth: ['Super Admin', 'Admin'],
|
||||
onClick: () => openStoreNavModal(row),
|
||||
},
|
||||
{
|
||||
label: '分账设置',
|
||||
type: 'link',
|
||||
icon: 'ant-design:percentage-outlined',
|
||||
size: 'small',
|
||||
auth: ['Super Admin', 'Admin'],
|
||||
onClick: () => openLedgerSplitModal(row),
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
type: 'link',
|
||||
|
||||
@@ -20,6 +20,7 @@ import DoctorLoginConfigPanel from './components/doctor-login-config-panel.vue';
|
||||
import FreeShippingConfigPanel from './components/free-shipping-config-panel.vue';
|
||||
import HomeDisplayConfigPanel from './components/home-display-config-panel.vue';
|
||||
import InputAuditConfigPanel from './components/input-audit-config-panel.vue';
|
||||
import ImageTransferConfigPanel from './components/image-transfer-config-panel.vue';
|
||||
import InvoiceNoticeConfigPanel from './components/invoice-notice-config-panel.vue';
|
||||
import LogisticsConfigPanel from './components/logistics-config-panel.vue';
|
||||
import MedicalRecordConfigPanel from './components/medical-record-config-panel.vue';
|
||||
@@ -30,6 +31,7 @@ import PriceAdjustConfigPanel from './components/price-adjust-config-panel.vue';
|
||||
import PublicAccountPayConfigPanel from './components/public-account-pay-config-panel.vue';
|
||||
import SalespersonConfigPanel from './components/salesperson-config-panel.vue';
|
||||
import WorkbenchConfigPanel from './components/workbench-config-panel.vue';
|
||||
import WithdrawalRulesConfigPanel from './components/withdrawal-rules-config-panel.vue';
|
||||
import WxWorkbenchDefaultFavPanel from './components/wx-workbench-default-fav-panel.vue';
|
||||
|
||||
defineOptions({ name: 'SystemConfig' });
|
||||
@@ -137,6 +139,22 @@ const menuItems: MenuItem[] = [
|
||||
title: '审核',
|
||||
component: InputAuditConfigPanel,
|
||||
},
|
||||
{
|
||||
key: 'image_transfer',
|
||||
title: '传方',
|
||||
component: ImageTransferConfigPanel,
|
||||
},
|
||||
{
|
||||
key: 'finance',
|
||||
title: '财务',
|
||||
tabs: [
|
||||
{
|
||||
key: 'withdrawal_rules',
|
||||
title: '提现规则',
|
||||
component: WithdrawalRulesConfigPanel,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'ai_model',
|
||||
title: 'AI模型',
|
||||
|
||||
@@ -4,6 +4,11 @@ export default defineConfig(async () => {
|
||||
return {
|
||||
application: {},
|
||||
vite: {
|
||||
build: {
|
||||
modulePreload: {
|
||||
polyfill: false
|
||||
}
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: [
|
||||
'@vben/plugins/echarts',
|
||||
|
||||
Reference in New Issue
Block a user