feat: 中药导入模块修复、部门财务的优化
This commit is contained in:
@@ -145,6 +145,7 @@ alwaysApply: true
|
|||||||
|
|
||||||
- 表单必须用 `useVbenForm`,禁止直接 `<form>` 或 antd `Form`
|
- 表单必须用 `useVbenForm`,禁止直接 `<form>` 或 antd `Form`
|
||||||
- 动态修改 schema 必须用 `formApi.updateSchema([...])`,**vben form 没有 `setComponentProps` 这个方法**(这是已踩过的坑)
|
- 动态修改 schema 必须用 `formApi.updateSchema([...])`,**vben form 没有 `setComponentProps` 这个方法**(这是已踩过的坑)
|
||||||
|
- 重置表单用 `formApi.resetForm()`,**严禁 `formApi.resetFields()`**(那是 antd Form 的 API,Vben Form 没有,会报 `resetFields is not a function`;已踩过坑)
|
||||||
- 取值/赋值用 `formApi.getValues()` / `formApi.setValues(obj)`,禁止自己 `v-model` 收集
|
- 取值/赋值用 `formApi.getValues()` / `formApi.setValues(obj)`,禁止自己 `v-model` 收集
|
||||||
- 校验用 `formApi.validate().then(e => { if (e.valid) {...} })` 或 `formApi.validateAndSubmitForm()`
|
- 校验用 `formApi.validate().then(e => { if (e.valid) {...} })` 或 `formApi.validateAndSubmitForm()`
|
||||||
- 必填规则用字符串 `'required'` / `'selectRequired'`,规则在 `#/adapter/form.ts` 的 `defineRules` 注册
|
- 必填规则用字符串 `'required'` / `'selectRequired'`,规则在 `#/adapter/form.ts` 的 `defineRules` 注册
|
||||||
|
|||||||
@@ -77,7 +77,11 @@ export async function updateZoneCategory(data: Record<string, any>) {
|
|||||||
* 更新最低调整金额
|
* 更新最低调整金额
|
||||||
* @param data
|
* @param data
|
||||||
*/
|
*/
|
||||||
export async function updateMinAdjustPrice(data: { id: number; min_adjust_price: number | null }) {
|
export async function updateMinAdjustPrice(data: {
|
||||||
|
id: number;
|
||||||
|
min_adjust_price: number | null;
|
||||||
|
max_adjust_price?: number | null;
|
||||||
|
}) {
|
||||||
return requestClient.post<any>(`${prefix}update-min-adjust-price`, data);
|
return requestClient.post<any>(`${prefix}update-min-adjust-price`, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,23 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
/**
|
||||||
import { Form, InputNumber, message } from 'ant-design-vue';
|
* 设置药品改价底价与最高价
|
||||||
|
* 商户改价、订单调价须落在该区间;可按总仓售价快捷填入上下 30%
|
||||||
|
*/
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
import { useVbenModal } from '@vben/common-ui';
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Button, Form, InputNumber, message } from 'ant-design-vue';
|
||||||
|
|
||||||
import { updateMinAdjustPrice } from '../api';
|
import { updateMinAdjustPrice } from '../api';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'MinPriceModal',
|
name: 'MinPriceModal',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** 相对总仓售价的浮动比例(上下各 30%) */
|
||||||
|
const CENTRAL_PRICE_RANGE_RATIO = 0.3;
|
||||||
|
|
||||||
const formRef = ref();
|
const formRef = ref();
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
@@ -15,9 +25,44 @@ const formData = ref<{
|
|||||||
id?: number;
|
id?: number;
|
||||||
drugName?: string;
|
drugName?: string;
|
||||||
currentMinPrice?: number | null;
|
currentMinPrice?: number | null;
|
||||||
|
currentMaxPrice?: number | null;
|
||||||
|
/** 总仓售价,用于快捷填区间 */
|
||||||
|
centralPrice?: number | null;
|
||||||
}>({});
|
}>({});
|
||||||
|
|
||||||
const minPrice = ref<number | null>(null);
|
const minPrice = ref<number | null>(null);
|
||||||
|
const maxPrice = ref<number | null>(null);
|
||||||
|
|
||||||
|
/** 是否有可用总仓售价 */
|
||||||
|
const hasCentralPrice = computed(() => {
|
||||||
|
const p = Number(formData.value.centralPrice);
|
||||||
|
return Number.isFinite(p) && p > 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 预览:底价 = 售价×0.7,最高价 = 售价×1.3 */
|
||||||
|
const previewRange = computed(() => {
|
||||||
|
if (!hasCentralPrice.value) return null;
|
||||||
|
const base = Number(formData.value.centralPrice);
|
||||||
|
const min = Number((base * (1 - CENTRAL_PRICE_RANGE_RATIO)).toFixed(2));
|
||||||
|
const max = Number((base * (1 + CENTRAL_PRICE_RANGE_RATIO)).toFixed(2));
|
||||||
|
return { min, max, base };
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快捷填入总仓售价上下 30% 区间
|
||||||
|
* 为什么:运营批量设价时以总仓建议售价为基准,避免手工算错
|
||||||
|
*/
|
||||||
|
function fillByCentralPriceRange() {
|
||||||
|
if (!previewRange.value) {
|
||||||
|
message.warning('暂无总仓售价,无法快捷填入');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
minPrice.value = previewRange.value.min;
|
||||||
|
maxPrice.value = previewRange.value.max;
|
||||||
|
message.success(
|
||||||
|
`已填入:¥${previewRange.value.min.toFixed(2)} ~ ¥${previewRange.value.max.toFixed(2)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const [ModalComponent, modalApi] = useVbenModal({
|
const [ModalComponent, modalApi] = useVbenModal({
|
||||||
class: 'w-[500px]',
|
class: 'w-[500px]',
|
||||||
@@ -30,19 +75,30 @@ const [ModalComponent, modalApi] = useVbenModal({
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await formRef.value.validate();
|
await formRef.value.validate();
|
||||||
} catch (error) {
|
} catch {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!formData.value.id || minPrice.value === null) {
|
if (!formData.value.id || minPrice.value === null) {
|
||||||
message.error('参数不完整');
|
message.error('参数不完整');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
maxPrice.value !== null &&
|
||||||
|
maxPrice.value > 0 &&
|
||||||
|
minPrice.value > maxPrice.value
|
||||||
|
) {
|
||||||
|
message.error('最高价不能低于底价');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
await updateMinAdjustPrice({
|
await updateMinAdjustPrice({
|
||||||
id: formData.value.id,
|
id: formData.value.id,
|
||||||
min_adjust_price: minPrice.value,
|
min_adjust_price: minPrice.value,
|
||||||
|
// 始终提交最高价字段:null/0 表示清空上限
|
||||||
|
max_adjust_price:
|
||||||
|
maxPrice.value && maxPrice.value > 0 ? maxPrice.value : 0,
|
||||||
});
|
});
|
||||||
message.success('设置成功');
|
message.success('设置成功');
|
||||||
modalApi.close();
|
modalApi.close();
|
||||||
@@ -59,45 +115,76 @@ const [ModalComponent, modalApi] = useVbenModal({
|
|||||||
},
|
},
|
||||||
onOpenChange(isOpen: boolean) {
|
onOpenChange(isOpen: boolean) {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
const data = modalApi.getData<typeof formData.value & { gridApi?: any }>();
|
const data = modalApi.getData<
|
||||||
|
typeof formData.value & {
|
||||||
|
gridApi?: any;
|
||||||
|
min_adjust_price?: number | null;
|
||||||
|
max_adjust_price?: number | null;
|
||||||
|
central_price?: number | null;
|
||||||
|
drug_name?: string;
|
||||||
|
}
|
||||||
|
>();
|
||||||
if (data) {
|
if (data) {
|
||||||
|
const central = Number(
|
||||||
|
data.centralPrice ?? data.central_price ?? 0,
|
||||||
|
);
|
||||||
formData.value = {
|
formData.value = {
|
||||||
id: data.id,
|
id: data.id,
|
||||||
drugName: data.drugName || data.drug_name,
|
drugName: data.drugName || data.drug_name,
|
||||||
currentMinPrice: data.currentMinPrice || data.min_adjust_price || null,
|
currentMinPrice: data.currentMinPrice || data.min_adjust_price || null,
|
||||||
|
currentMaxPrice: data.currentMaxPrice || data.max_adjust_price || null,
|
||||||
|
centralPrice: central > 0 ? central : null,
|
||||||
};
|
};
|
||||||
minPrice.value = formData.value.currentMinPrice;
|
minPrice.value = formData.value.currentMinPrice;
|
||||||
|
maxPrice.value = formData.value.currentMaxPrice;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
formData.value = {};
|
formData.value = {};
|
||||||
minPrice.value = null;
|
minPrice.value = null;
|
||||||
|
maxPrice.value = null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ModalComponent
|
<ModalComponent title="设置改价区间(底价 / 最高价)" :loading="loading">
|
||||||
title="设置最低调整金额"
|
|
||||||
:loading="loading"
|
|
||||||
>
|
|
||||||
<div v-if="formData.drugName" class="min-price-modal">
|
<div v-if="formData.drugName" class="min-price-modal">
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<p><strong>药品名称:</strong>{{ formData.drugName }}</p>
|
<p><strong>药品名称:</strong>{{ formData.drugName }}</p>
|
||||||
<p v-if="formData.currentMinPrice" class="mt-2">
|
<p v-if="hasCentralPrice" class="mt-1 text-sm text-gray-600">
|
||||||
<strong>当前最低调整金额:</strong>¥{{ Number(formData.currentMinPrice).toFixed(2) }}
|
总仓售价:¥{{ Number(formData.centralPrice).toFixed(2) }}
|
||||||
</p>
|
</p>
|
||||||
<p v-else class="mt-2" style="color: #999">
|
<p class="mt-2 text-sm text-gray-500">
|
||||||
<strong>当前最低调整金额:</strong>未设置
|
商户改价与订单调价须落在底价~最高价之间;最高价留空表示不限制上限。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Form ref="formRef" :model="{ minPrice }" layout="vertical">
|
<div class="mb-4">
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
ghost
|
||||||
|
:disabled="!hasCentralPrice"
|
||||||
|
@click="fillByCentralPriceRange"
|
||||||
|
>
|
||||||
|
快捷填入总仓售价 ±30%
|
||||||
|
</Button>
|
||||||
|
<p v-if="previewRange" class="mt-2 text-xs text-gray-500">
|
||||||
|
将填入 ¥{{ previewRange.min.toFixed(2) }} ~
|
||||||
|
¥{{ previewRange.max.toFixed(2) }}
|
||||||
|
(售价 ¥{{ previewRange.base.toFixed(2) }} × 0.7 / 1.3)
|
||||||
|
</p>
|
||||||
|
<p v-else class="mt-2 text-xs text-orange-500">
|
||||||
|
该药品暂无总仓售价,请先入库总仓后再使用快捷填入。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Form ref="formRef" :model="{ minPrice, maxPrice }" layout="vertical">
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="最低调整金额"
|
label="底价(最低调整金额)"
|
||||||
name="minPrice"
|
name="minPrice"
|
||||||
:rules="[
|
:rules="[
|
||||||
{ required: true, message: '请输入最低调整金额' },
|
{ required: true, message: '请输入底价' },
|
||||||
{ type: 'number', min: 0, message: '金额不能小于0' },
|
{ type: 'number', min: 0, message: '金额不能小于0' },
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
@@ -107,12 +194,19 @@ const [ModalComponent, modalApi] = useVbenModal({
|
|||||||
:precision="2"
|
:precision="2"
|
||||||
:step="0.01"
|
:step="0.01"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
placeholder="请输入最低调整金额(设置为0或留空表示不允许调整)"
|
placeholder="请输入底价(0 表示不允许调价)"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="最高价" name="maxPrice">
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="maxPrice"
|
||||||
|
:min="0"
|
||||||
|
:precision="2"
|
||||||
|
:step="0.01"
|
||||||
|
style="width: 100%"
|
||||||
|
placeholder="请输入最高价(留空或0表示不限制)"
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<p class="text-gray-500 text-sm mt-2">
|
|
||||||
提示:设置为0或留空表示该商品不允许调整价格
|
|
||||||
</p>
|
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
</ModalComponent>
|
</ModalComponent>
|
||||||
|
|||||||
@@ -64,8 +64,8 @@ export const gridOptions: VxeGridProps<RowType> = {
|
|||||||
{ field: 'status', title: '状态', width: 90, slots: { default: 'status' } },
|
{ field: 'status', title: '状态', width: 90, slots: { default: 'status' } },
|
||||||
{
|
{
|
||||||
field: 'min_adjust_price',
|
field: 'min_adjust_price',
|
||||||
title: '最低调整金额',
|
title: '改价区间',
|
||||||
width: 120,
|
width: 140,
|
||||||
slots: { default: 'min_adjust_price' },
|
slots: { default: 'min_adjust_price' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -157,6 +157,9 @@ const openMinPriceModal = (row: any) => {
|
|||||||
id: row.id,
|
id: row.id,
|
||||||
drugName: row.drug_name,
|
drugName: row.drug_name,
|
||||||
min_adjust_price: row.min_adjust_price,
|
min_adjust_price: row.min_adjust_price,
|
||||||
|
max_adjust_price: row.max_adjust_price,
|
||||||
|
// 总仓售价:供弹窗「±30%」快捷填入
|
||||||
|
central_price: resolveCentralPrice(row),
|
||||||
gridApi,
|
gridApi,
|
||||||
});
|
});
|
||||||
minPriceModalApi.open();
|
minPriceModalApi.open();
|
||||||
@@ -311,9 +314,13 @@ const openExcelUploadModal = () => {
|
|||||||
class="text-primary cursor-pointer hover:underline"
|
class="text-primary cursor-pointer hover:underline"
|
||||||
@click="openMinPriceModal(row)"
|
@click="openMinPriceModal(row)"
|
||||||
>
|
>
|
||||||
<span v-if="row.min_adjust_price && row.min_adjust_price > 0">
|
<template v-if="row.min_adjust_price && row.min_adjust_price > 0">
|
||||||
¥{{ Number(row.min_adjust_price).toFixed(2) }}
|
¥{{ Number(row.min_adjust_price).toFixed(2) }}
|
||||||
</span>
|
<span v-if="row.max_adjust_price && row.max_adjust_price > 0">
|
||||||
|
~ ¥{{ Number(row.max_adjust_price).toFixed(2) }}
|
||||||
|
</span>
|
||||||
|
<span v-else> ~ 不限</span>
|
||||||
|
</template>
|
||||||
<span v-else style="color: #999">未设置</span>
|
<span v-else style="color: #999">未设置</span>
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
@@ -344,7 +351,7 @@ const openExcelUploadModal = () => {
|
|||||||
onClick: openBindList.bind(null, row),
|
onClick: openBindList.bind(null, row),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '设置底价',
|
label: '设置改价区间',
|
||||||
type: 'link',
|
type: 'link',
|
||||||
icon: 'ant-design:dollar-outlined',
|
icon: 'ant-design:dollar-outlined',
|
||||||
size: 'small',
|
size: 'small',
|
||||||
|
|||||||
@@ -44,11 +44,21 @@ export async function getWarehouseDrugManagementInfo(id: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 导出仓库药品药品
|
* 导出仓库药品(旧:后端二进制,保留兼容)
|
||||||
*/
|
*/
|
||||||
export async function exportWarehouseDrugManagementApi(params?: { type?: number }) {
|
export async function exportWarehouseDrugManagementApi(params?: { type?: number }) {
|
||||||
return requestClient.download(`${prefix}export`, { params });
|
return requestClient.download(`${prefix}export`, { params });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出 JSON 数据(供前端 ExcelJS,对齐订单 export-data)
|
||||||
|
*/
|
||||||
|
export async function getWarehouseExportDataApi(params?: { type?: number }) {
|
||||||
|
return requestClient.get<{
|
||||||
|
total: number;
|
||||||
|
rows: Array<Record<string, unknown>>;
|
||||||
|
}>(`${prefix}export-data`, { params });
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* 导出仓库药品药品 - 模板
|
* 导出仓库药品药品 - 模板
|
||||||
*/
|
*/
|
||||||
@@ -110,22 +120,25 @@ export interface WarehouseBatchPriceItem {
|
|||||||
drug_name: string;
|
drug_name: string;
|
||||||
drug_number: string;
|
drug_number: string;
|
||||||
pinyin_simple: string;
|
pinyin_simple: string;
|
||||||
|
unit_id?: number;
|
||||||
|
unit_name?: string;
|
||||||
market_price: number | string;
|
market_price: number | string;
|
||||||
price: number | string;
|
price: number | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 批量改价预览:全量仓库药品 */
|
/** 批量改价预览:全量仓库药品 */
|
||||||
export async function getAllForBatchPriceApi(params: { type: number }) {
|
export async function getAllForBatchPriceApi(params: { type: number }) {
|
||||||
return requestClient.get<{ items: WarehouseBatchPriceItem[] }>(
|
return requestClient.get<{
|
||||||
`${prefix}list-all-for-batch-price`,
|
items: WarehouseBatchPriceItem[];
|
||||||
{ params },
|
unit_options?: { value: number; label: string }[];
|
||||||
);
|
}>(`${prefix}list-all-for-batch-price`, { params });
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BatchPriceUpdateItem {
|
export interface BatchPriceUpdateItem {
|
||||||
id: number;
|
id: number;
|
||||||
market_price: number;
|
market_price: number;
|
||||||
price: number;
|
price: number;
|
||||||
|
unit_name?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BatchPriceCreateItem {
|
export interface BatchPriceCreateItem {
|
||||||
@@ -133,6 +146,7 @@ export interface BatchPriceCreateItem {
|
|||||||
drug_number: string;
|
drug_number: string;
|
||||||
market_price: number;
|
market_price: number;
|
||||||
price: number;
|
price: number;
|
||||||
|
unit_name?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 批量改价预览:应用更新与新增 */
|
/** 批量改价预览:应用更新与新增 */
|
||||||
|
|||||||
@@ -19,17 +19,23 @@ import { downloadByData } from '#/util/tool';
|
|||||||
import {
|
import {
|
||||||
applyBatchPriceApi,
|
applyBatchPriceApi,
|
||||||
getAllForBatchPriceApi,
|
getAllForBatchPriceApi,
|
||||||
|
getWarehouseExportDataApi,
|
||||||
type WarehouseBatchPriceItem,
|
type WarehouseBatchPriceItem,
|
||||||
} from '../api';
|
} from '../api';
|
||||||
import { exportWarehousePriceExcel } from '../utils/exportWarehousePriceExcel';
|
import {
|
||||||
|
exportWarehouseListExcel,
|
||||||
|
type WarehouseExportRow,
|
||||||
|
} from '../utils/exportWarehousePriceExcel';
|
||||||
import { parseWarehousePriceExcelBuffer } from '../utils/parseWarehousePriceExcel';
|
import { parseWarehousePriceExcelBuffer } from '../utils/parseWarehousePriceExcel';
|
||||||
import {
|
import {
|
||||||
getPriceDelta,
|
getPriceDelta,
|
||||||
isFieldChanged,
|
isFieldChanged,
|
||||||
isMarketGtPrice,
|
isMarketGtPrice,
|
||||||
isPriceChanged,
|
isPriceChanged,
|
||||||
|
isUnitChanged,
|
||||||
normalizePricePair,
|
normalizePricePair,
|
||||||
parsePriceValue,
|
parsePriceValue,
|
||||||
|
reconcileExcelPrice,
|
||||||
type PricePair,
|
type PricePair,
|
||||||
} from '../utils/priceCompare';
|
} from '../utils/priceCompare';
|
||||||
import PriceCompareCardGrid, {
|
import PriceCompareCardGrid, {
|
||||||
@@ -60,10 +66,13 @@ const newRows = ref<
|
|||||||
drug_number: string;
|
drug_number: string;
|
||||||
market_price: number;
|
market_price: number;
|
||||||
price: number;
|
price: number;
|
||||||
|
unit_name: string;
|
||||||
}>
|
}>
|
||||||
>([]);
|
>([]);
|
||||||
const unmatchedRows = ref<UnmatchedRow[]>([]);
|
const unmatchedRows = ref<UnmatchedRow[]>([]);
|
||||||
const excelTouchedIds = ref<Set<number>>(new Set());
|
const excelTouchedIds = ref<Set<number>>(new Set());
|
||||||
|
/** 已知单位名称集合,用于标记「待建」 */
|
||||||
|
const knownUnitNames = ref<Set<string>>(new Set());
|
||||||
let tempIdCounter = -1;
|
let tempIdCounter = -1;
|
||||||
let loadAllPromise: Promise<void> | null = null;
|
let loadAllPromise: Promise<void> | null = null;
|
||||||
|
|
||||||
@@ -74,6 +83,7 @@ function buildSnapshot(items: WarehouseBatchPriceItem[]) {
|
|||||||
const pair = normalizePricePair({
|
const pair = normalizePricePair({
|
||||||
market_price: parsePriceValue(item.market_price),
|
market_price: parsePriceValue(item.market_price),
|
||||||
price: parsePriceValue(item.price),
|
price: parsePriceValue(item.price),
|
||||||
|
unit_name: item.unit_name ?? '',
|
||||||
});
|
});
|
||||||
init[item.id] = { ...pair };
|
init[item.id] = { ...pair };
|
||||||
loc[item.id] = { ...pair };
|
loc[item.id] = { ...pair };
|
||||||
@@ -91,6 +101,7 @@ function resetState() {
|
|||||||
newRows.value = [];
|
newRows.value = [];
|
||||||
unmatchedRows.value = [];
|
unmatchedRows.value = [];
|
||||||
excelTouchedIds.value = new Set();
|
excelTouchedIds.value = new Set();
|
||||||
|
knownUnitNames.value = new Set();
|
||||||
parseHint.value = '';
|
parseHint.value = '';
|
||||||
parsedReady.value = false;
|
parsedReady.value = false;
|
||||||
filterTab.value = 'all';
|
filterTab.value = 'all';
|
||||||
@@ -109,22 +120,44 @@ function buildCompareRow(
|
|||||||
orig: PricePair,
|
orig: PricePair,
|
||||||
isNew: boolean,
|
isNew: boolean,
|
||||||
): PriceCompareRow {
|
): PriceCompareRow {
|
||||||
|
const normalizedOrig = normalizePricePair(orig);
|
||||||
|
const normalizedCurrent = normalizePricePair(current);
|
||||||
|
const unitChanged = isUnitChanged(
|
||||||
|
normalizedOrig.unit_name,
|
||||||
|
normalizedCurrent.unit_name,
|
||||||
|
);
|
||||||
|
const unitName = (normalizedCurrent.unit_name ?? '').trim();
|
||||||
|
const pendingCreate =
|
||||||
|
unitName !== '' && !knownUnitNames.value.has(unitName);
|
||||||
return {
|
return {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
drug_id: item.drug_id,
|
drug_id: item.drug_id,
|
||||||
drug_name: item.drug_name,
|
drug_name: item.drug_name,
|
||||||
drug_number: item.drug_number,
|
drug_number: item.drug_number,
|
||||||
pinyin_simple: item.pinyin_simple,
|
pinyin_simple: item.pinyin_simple,
|
||||||
orig_market_price: orig.market_price,
|
orig_market_price: normalizedOrig.market_price,
|
||||||
orig_price: orig.price,
|
orig_price: normalizedOrig.price,
|
||||||
market_price: current.market_price,
|
orig_unit_name: normalizedOrig.unit_name ?? '',
|
||||||
price: current.price,
|
market_price: normalizedCurrent.market_price,
|
||||||
_marketDelta: getPriceDelta(orig.market_price, current.market_price),
|
price: normalizedCurrent.price,
|
||||||
_priceDelta: getPriceDelta(orig.price, current.price),
|
unit_name: normalizedCurrent.unit_name ?? '',
|
||||||
_marketChanged: isFieldChanged(orig.market_price, current.market_price),
|
_marketDelta: getPriceDelta(
|
||||||
_priceChanged: isFieldChanged(orig.price, current.price),
|
normalizedOrig.market_price,
|
||||||
_changed: isNew || isPriceChanged(orig, current),
|
normalizedCurrent.market_price,
|
||||||
_marketGtPrice: isMarketGtPrice(current),
|
),
|
||||||
|
_priceDelta: getPriceDelta(normalizedOrig.price, normalizedCurrent.price),
|
||||||
|
_marketChanged: isFieldChanged(
|
||||||
|
normalizedOrig.market_price,
|
||||||
|
normalizedCurrent.market_price,
|
||||||
|
),
|
||||||
|
_priceChanged: isFieldChanged(
|
||||||
|
normalizedOrig.price,
|
||||||
|
normalizedCurrent.price,
|
||||||
|
),
|
||||||
|
_unitChanged: unitChanged,
|
||||||
|
_unitPendingCreate: pendingCreate,
|
||||||
|
_changed: isNew || isPriceChanged(normalizedOrig, normalizedCurrent),
|
||||||
|
_marketGtPrice: isMarketGtPrice(normalizedCurrent),
|
||||||
_isNew: isNew,
|
_isNew: isNew,
|
||||||
_fromExcel: isNew ? true : undefined,
|
_fromExcel: isNew ? true : undefined,
|
||||||
};
|
};
|
||||||
@@ -134,11 +167,13 @@ const compareRows = computed<PriceCompareRow[]>(() => {
|
|||||||
const existingRows: PriceCompareRow[] = warehouseItems.value
|
const existingRows: PriceCompareRow[] = warehouseItems.value
|
||||||
.filter((item) => excelTouchedIds.value.has(item.id))
|
.filter((item) => excelTouchedIds.value.has(item.id))
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
const current = localPrices.value[item.id] ?? normalizePricePair({
|
const fallback = normalizePricePair({
|
||||||
market_price: parsePriceValue(item.market_price),
|
market_price: parsePriceValue(item.market_price),
|
||||||
price: parsePriceValue(item.price),
|
price: parsePriceValue(item.price),
|
||||||
|
unit_name: item.unit_name ?? '',
|
||||||
});
|
});
|
||||||
const orig = initialSnapshot.value[item.id] ?? current;
|
const current = localPrices.value[item.id] ?? fallback;
|
||||||
|
const orig = initialSnapshot.value[item.id] ?? fallback;
|
||||||
return buildCompareRow(item, current, orig, false);
|
return buildCompareRow(item, current, orig, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -149,8 +184,12 @@ const compareRows = computed<PriceCompareRow[]>(() => {
|
|||||||
drug_name: row.drug_name,
|
drug_name: row.drug_name,
|
||||||
drug_number: row.drug_number,
|
drug_number: row.drug_number,
|
||||||
},
|
},
|
||||||
{ market_price: row.market_price, price: row.price },
|
{
|
||||||
{ market_price: 0, price: 0 },
|
market_price: row.market_price,
|
||||||
|
price: row.price,
|
||||||
|
unit_name: row.unit_name,
|
||||||
|
},
|
||||||
|
{ market_price: 0, price: 0, unit_name: '' },
|
||||||
true,
|
true,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -225,6 +264,9 @@ async function loadAllDrugs() {
|
|||||||
try {
|
try {
|
||||||
const res = await getAllForBatchPriceApi({ type: productType.value });
|
const res = await getAllForBatchPriceApi({ type: productType.value });
|
||||||
warehouseItems.value = res?.items ?? [];
|
warehouseItems.value = res?.items ?? [];
|
||||||
|
knownUnitNames.value = new Set(
|
||||||
|
(res?.unit_options ?? []).map((u) => String(u.label).trim()).filter(Boolean),
|
||||||
|
);
|
||||||
buildSnapshot(warehouseItems.value);
|
buildSnapshot(warehouseItems.value);
|
||||||
} catch {
|
} catch {
|
||||||
message.error('加载仓库药品失败');
|
message.error('加载仓库药品失败');
|
||||||
@@ -255,24 +297,37 @@ async function handleFileChange(info: { file: UploadFile }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载标准模板:与列表页「导出」同一套 export-data + 列结构,
|
||||||
|
* 避免改价模板列与导出不一致导致解析误报变更
|
||||||
|
*/
|
||||||
async function downloadTemplate() {
|
async function downloadTemplate() {
|
||||||
if (warehouseItems.value.length === 0) {
|
|
||||||
message.warning('暂无药品数据');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const buffer = await exportWarehousePriceExcel(warehouseItems.value);
|
const res = await getWarehouseExportDataApi({ type: productType.value });
|
||||||
|
const rows = (res?.rows ?? []) as WarehouseExportRow[];
|
||||||
|
if (rows.length === 0) {
|
||||||
|
message.warning('暂无药品数据');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const buffer = await exportWarehouseListExcel(rows, '仓库商品');
|
||||||
downloadByData(
|
downloadByData(
|
||||||
buffer,
|
buffer,
|
||||||
'萧康云医-中药仓库改价模板.xlsx',
|
'萧康云医-仓库商品导出.xlsx',
|
||||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
);
|
);
|
||||||
message.success('模板已下载');
|
message.success(`模板已下载(${res?.total ?? rows.length} 条,与导出一致)`);
|
||||||
} catch {
|
} catch {
|
||||||
message.error('模板下载失败');
|
message.error('模板下载失败');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function findWarehouseByDrugId(drugId: number) {
|
||||||
|
if (drugId <= 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return warehouseItems.value.find((d) => d.drug_id === drugId);
|
||||||
|
}
|
||||||
|
|
||||||
function findWarehouseByNumber(number: string) {
|
function findWarehouseByNumber(number: string) {
|
||||||
const n = number.trim();
|
const n = number.trim();
|
||||||
if (!n) {
|
if (!n) {
|
||||||
@@ -300,7 +355,7 @@ async function runParse() {
|
|||||||
syncModalFooterState();
|
syncModalFooterState();
|
||||||
try {
|
try {
|
||||||
const buf = await selectedFile.value.arrayBuffer();
|
const buf = await selectedFile.value.arrayBuffer();
|
||||||
const { items, invalidCount, rowCount } =
|
const { items, invalidCount, rowCount, hasUnitColumn } =
|
||||||
await parseWarehousePriceExcelBuffer(buf);
|
await parseWarehousePriceExcelBuffer(buf);
|
||||||
|
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
@@ -312,6 +367,9 @@ async function runParse() {
|
|||||||
rowCount > 5000
|
rowCount > 5000
|
||||||
? `共 ${rowCount} 行,已按上限处理前 5000 行。`
|
? `共 ${rowCount} 行,已按上限处理前 5000 行。`
|
||||||
: `共解析 ${items.length} 条有效行。`;
|
: `共解析 ${items.length} 条有效行。`;
|
||||||
|
if (!hasUnitColumn) {
|
||||||
|
parseHint.value += '(未识别到单位列,单位保持原值)';
|
||||||
|
}
|
||||||
|
|
||||||
const nextUnmatched: UnmatchedRow[] = [];
|
const nextUnmatched: UnmatchedRow[] = [];
|
||||||
const nextNew: typeof newRows.value = [];
|
const nextNew: typeof newRows.value = [];
|
||||||
@@ -320,31 +378,41 @@ async function runParse() {
|
|||||||
let changedExistingCount = 0;
|
let changedExistingCount = 0;
|
||||||
|
|
||||||
for (const row of items) {
|
for (const row of items) {
|
||||||
let matched = findWarehouseByNumber(row.drug_number);
|
// 优先药品ID,避免同名/编码漂移误匹配
|
||||||
|
let matched =
|
||||||
|
findWarehouseByDrugId(row.drug_id) ||
|
||||||
|
findWarehouseByNumber(row.drug_number);
|
||||||
if (!matched) {
|
if (!matched) {
|
||||||
matched = findWarehouseByName(row.drug_name);
|
matched = findWarehouseByName(row.drug_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (matched) {
|
if (matched) {
|
||||||
nextTouched.add(matched.id);
|
nextTouched.add(matched.id);
|
||||||
const orig = initialSnapshot.value[matched.id];
|
const orig = initialSnapshot.value[matched.id] ?? {
|
||||||
nextLocal[matched.id] = {
|
market_price: parsePriceValue(matched.market_price),
|
||||||
market_price: row.market_price,
|
price: parsePriceValue(matched.price),
|
||||||
price: row.price,
|
unit_name: matched.unit_name ?? '',
|
||||||
};
|
};
|
||||||
if (
|
// 无单位列或单元格为空:保留原单位,避免误报单位变更
|
||||||
orig &&
|
const excelUnit = (row.unit_name ?? '').trim();
|
||||||
isPriceChanged(orig, {
|
const unitName =
|
||||||
market_price: row.market_price,
|
hasUnitColumn && excelUnit !== ''
|
||||||
price: row.price,
|
? excelUnit
|
||||||
})
|
: (orig.unit_name ?? matched.unit_name ?? '');
|
||||||
) {
|
// 价格与快照对账,消化 Excel 一分钱浮点误差
|
||||||
|
const nextPair = normalizePricePair({
|
||||||
|
market_price: reconcileExcelPrice(row.market_price, orig.market_price),
|
||||||
|
price: reconcileExcelPrice(row.price, orig.price),
|
||||||
|
unit_name: unitName,
|
||||||
|
});
|
||||||
|
nextLocal[matched.id] = nextPair;
|
||||||
|
if (isPriceChanged(orig, nextPair)) {
|
||||||
changedExistingCount += 1;
|
changedExistingCount += 1;
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (row.drug_name || row.drug_number) {
|
if (row.drug_name || row.drug_number || row.drug_id > 0) {
|
||||||
tempIdCounter -= 1;
|
tempIdCounter -= 1;
|
||||||
nextNew.push({
|
nextNew.push({
|
||||||
tempId: tempIdCounter,
|
tempId: tempIdCounter,
|
||||||
@@ -352,6 +420,7 @@ async function runParse() {
|
|||||||
drug_number: row.drug_number,
|
drug_number: row.drug_number,
|
||||||
market_price: row.market_price,
|
market_price: row.market_price,
|
||||||
price: row.price,
|
price: row.price,
|
||||||
|
unit_name: (row.unit_name ?? '').trim(),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
nextUnmatched.push(row);
|
nextUnmatched.push(row);
|
||||||
@@ -368,7 +437,7 @@ async function runParse() {
|
|||||||
} else if (nextNew.length > 0) {
|
} else if (nextNew.length > 0) {
|
||||||
filterTab.value = 'new';
|
filterTab.value = 'new';
|
||||||
} else {
|
} else {
|
||||||
filterTab.value = 'all';
|
filterTab.value = 'unchanged';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (invalidCount > 0) {
|
if (invalidCount > 0) {
|
||||||
@@ -377,6 +446,9 @@ async function runParse() {
|
|||||||
if (nextNew.length > 0) {
|
if (nextNew.length > 0) {
|
||||||
message.info(`有 ${nextNew.length} 条药品将自动新增入库`);
|
message.info(`有 ${nextNew.length} 条药品将自动新增入库`);
|
||||||
}
|
}
|
||||||
|
if (changedExistingCount === 0 && nextNew.length === 0) {
|
||||||
|
message.info('未检测到价格或单位变更');
|
||||||
|
}
|
||||||
|
|
||||||
parsedReady.value = true;
|
parsedReady.value = true;
|
||||||
} catch {
|
} catch {
|
||||||
@@ -391,6 +463,7 @@ function syncRowFromGrid(row: PriceCompareRow) {
|
|||||||
const normalized = normalizePricePair({
|
const normalized = normalizePricePair({
|
||||||
market_price: parsePriceValue(row.market_price),
|
market_price: parsePriceValue(row.market_price),
|
||||||
price: parsePriceValue(row.price),
|
price: parsePriceValue(row.price),
|
||||||
|
unit_name: row.unit_name,
|
||||||
});
|
});
|
||||||
if (row._isNew) {
|
if (row._isNew) {
|
||||||
newRows.value = newRows.value.map((item) =>
|
newRows.value = newRows.value.map((item) =>
|
||||||
@@ -399,6 +472,7 @@ function syncRowFromGrid(row: PriceCompareRow) {
|
|||||||
...item,
|
...item,
|
||||||
market_price: normalized.market_price,
|
market_price: normalized.market_price,
|
||||||
price: normalized.price,
|
price: normalized.price,
|
||||||
|
unit_name: normalized.unit_name ?? '',
|
||||||
}
|
}
|
||||||
: item,
|
: item,
|
||||||
);
|
);
|
||||||
@@ -435,6 +509,7 @@ async function handleRowSave(row: PriceCompareRow) {
|
|||||||
const saved = normalizePricePair({
|
const saved = normalizePricePair({
|
||||||
market_price: parsePriceValue(row.market_price),
|
market_price: parsePriceValue(row.market_price),
|
||||||
price: parsePriceValue(row.price),
|
price: parsePriceValue(row.price),
|
||||||
|
unit_name: row.unit_name,
|
||||||
});
|
});
|
||||||
|
|
||||||
savingRowId.value = row.id;
|
savingRowId.value = row.id;
|
||||||
@@ -449,6 +524,7 @@ async function handleRowSave(row: PriceCompareRow) {
|
|||||||
drug_number: row.drug_number,
|
drug_number: row.drug_number,
|
||||||
market_price: saved.market_price,
|
market_price: saved.market_price,
|
||||||
price: saved.price,
|
price: saved.price,
|
||||||
|
unit_name: saved.unit_name,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
notify: false,
|
notify: false,
|
||||||
@@ -462,6 +538,7 @@ async function handleRowSave(row: PriceCompareRow) {
|
|||||||
id: row.id,
|
id: row.id,
|
||||||
market_price: saved.market_price,
|
market_price: saved.market_price,
|
||||||
price: saved.price,
|
price: saved.price,
|
||||||
|
unit_name: saved.unit_name,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
creates: [],
|
creates: [],
|
||||||
@@ -475,9 +552,17 @@ async function handleRowSave(row: PriceCompareRow) {
|
|||||||
...localPrices.value,
|
...localPrices.value,
|
||||||
[row.id]: { ...saved },
|
[row.id]: { ...saved },
|
||||||
};
|
};
|
||||||
|
if (saved.unit_name) {
|
||||||
|
knownUnitNames.value.add(saved.unit_name);
|
||||||
|
}
|
||||||
warehouseItems.value = warehouseItems.value.map((item) =>
|
warehouseItems.value = warehouseItems.value.map((item) =>
|
||||||
item.id === row.id
|
item.id === row.id
|
||||||
? { ...item, market_price: saved.market_price, price: saved.price }
|
? {
|
||||||
|
...item,
|
||||||
|
market_price: saved.market_price,
|
||||||
|
price: saved.price,
|
||||||
|
unit_name: saved.unit_name,
|
||||||
|
}
|
||||||
: item,
|
: item,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -499,12 +584,14 @@ async function runBatchApply() {
|
|||||||
const prices = normalizePricePair({
|
const prices = normalizePricePair({
|
||||||
market_price: row.market_price,
|
market_price: row.market_price,
|
||||||
price: row.price,
|
price: row.price,
|
||||||
|
unit_name: row.unit_name,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
drug_name: row.drug_name,
|
drug_name: row.drug_name,
|
||||||
drug_number: row.drug_number,
|
drug_number: row.drug_number,
|
||||||
market_price: prices.market_price,
|
market_price: prices.market_price,
|
||||||
price: prices.price,
|
price: prices.price,
|
||||||
|
unit_name: prices.unit_name,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -522,8 +609,12 @@ async function runBatchApply() {
|
|||||||
drug_name: c.drug_name,
|
drug_name: c.drug_name,
|
||||||
drug_number: c.drug_number,
|
drug_number: c.drug_number,
|
||||||
},
|
},
|
||||||
{ market_price: c.market_price, price: c.price },
|
{
|
||||||
{ market_price: 0, price: 0 },
|
market_price: c.market_price,
|
||||||
|
price: c.price,
|
||||||
|
unit_name: c.unit_name,
|
||||||
|
},
|
||||||
|
{ market_price: 0, price: 0, unit_name: '' },
|
||||||
true,
|
true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -541,11 +632,13 @@ async function runBatchApply() {
|
|||||||
const prices = normalizePricePair({
|
const prices = normalizePricePair({
|
||||||
market_price: parsePriceValue(row.market_price),
|
market_price: parsePriceValue(row.market_price),
|
||||||
price: parsePriceValue(row.price),
|
price: parsePriceValue(row.price),
|
||||||
|
unit_name: row.unit_name,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
market_price: prices.market_price,
|
market_price: prices.market_price,
|
||||||
price: prices.price,
|
price: prices.price,
|
||||||
|
unit_name: prices.unit_name,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
creates,
|
creates,
|
||||||
@@ -568,7 +661,7 @@ async function runBatchApply() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ModalComp title="批量改价预览(中药总仓库)">
|
<ModalComp title="批量改价预览(总仓库)">
|
||||||
<!-- Wrap main content in transition for smooth View swapping -->
|
<!-- Wrap main content in transition for smooth View swapping -->
|
||||||
<Transition name="fade-slide" mode="out-in">
|
<Transition name="fade-slide" mode="out-in">
|
||||||
|
|
||||||
@@ -699,6 +792,7 @@ async function runBatchApply() {
|
|||||||
:rows="compareRows"
|
:rows="compareRows"
|
||||||
:saving-row-id="savingRowId"
|
:saving-row-id="savingRowId"
|
||||||
:unmatched-rows="unmatchedRows"
|
:unmatched-rows="unmatchedRows"
|
||||||
|
:show-unit="true"
|
||||||
@cell-change="syncRowFromGrid"
|
@cell-change="syncRowFromGrid"
|
||||||
@row-save="handleRowSave"
|
@row-save="handleRowSave"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { downloadByData } from '#/util/tool';
|
|||||||
import { useWarehouseDrugTypeRoute } from '../shared/useWarehouseDrugTypeRoute';
|
import { useWarehouseDrugTypeRoute } from '../shared/useWarehouseDrugTypeRoute';
|
||||||
import {
|
import {
|
||||||
deleteWarehouseDrugManagement,
|
deleteWarehouseDrugManagement,
|
||||||
exportWarehouseDrugManagementApi,
|
getWarehouseExportDataApi,
|
||||||
updateWarehouseDrugManagementStatusApi
|
updateWarehouseDrugManagementStatusApi
|
||||||
} from './api';
|
} from './api';
|
||||||
import FormModalDemo from './components/modal.vue';
|
import FormModalDemo from './components/modal.vue';
|
||||||
@@ -26,6 +26,10 @@ import ViewModeFloatButton from './components/ViewModeFloatButton.vue';
|
|||||||
import WarehouseDrugExcelView from './components/WarehouseDrugExcelView.vue';
|
import WarehouseDrugExcelView from './components/WarehouseDrugExcelView.vue';
|
||||||
import { formOptions } from './config/search';
|
import { formOptions } from './config/search';
|
||||||
import { createGridOptions } from './config/table';
|
import { createGridOptions } from './config/table';
|
||||||
|
import {
|
||||||
|
exportWarehouseListExcel,
|
||||||
|
type WarehouseExportRow,
|
||||||
|
} from './utils/exportWarehousePriceExcel';
|
||||||
import {
|
import {
|
||||||
readWarehouseViewMode,
|
readWarehouseViewMode,
|
||||||
writeWarehouseViewMode,
|
writeWarehouseViewMode,
|
||||||
@@ -129,13 +133,26 @@ const deleteApi = (row: any) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* 导出
|
* 导出:后端 export-data 出 JSON,前端 ExcelJS 生成文件(对齐订单导出)
|
||||||
*/
|
*/
|
||||||
const passApplication = () => {
|
const passApplication = async () => {
|
||||||
exportWarehouseDrugManagementApi({ type: productType.value }).then((res) => {
|
try {
|
||||||
downloadByData(res.data, '萧康云医-仓库商品导出.xlsx');
|
const res = await getWarehouseExportDataApi({ type: productType.value });
|
||||||
message.success('导出成功!');
|
const rows = (res?.rows ?? []) as WarehouseExportRow[];
|
||||||
});
|
if (rows.length === 0) {
|
||||||
|
message.warning('暂无药品数据');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const buffer = await exportWarehouseListExcel(rows, '仓库商品');
|
||||||
|
downloadByData(
|
||||||
|
buffer,
|
||||||
|
'萧康云医-仓库商品导出.xlsx',
|
||||||
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
);
|
||||||
|
message.success(`导出成功(${res?.total ?? rows.length} 条)`);
|
||||||
|
} catch {
|
||||||
|
message.error('导出失败');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const openExcelUploadModal = (type = 1) => {
|
const openExcelUploadModal = (type = 1) => {
|
||||||
@@ -215,13 +232,14 @@ function onSpreadsheetConfigSaved() {
|
|||||||
// auth: ['超级西(中成)药', 'sys:user:save'],
|
// auth: ['超级西(中成)药', 'sys:user:save'],
|
||||||
onClick: openExcelUploadModal.bind(null, 1),
|
onClick: openExcelUploadModal.bind(null, 1),
|
||||||
},
|
},
|
||||||
{
|
// 旧「批量修改价格」直传入口已停用,改走「批量改价预览」
|
||||||
label: '批量修改价格',
|
// {
|
||||||
type: 'primary',
|
// label: '批量修改价格',
|
||||||
icon: 'ix:export-check',
|
// type: 'primary',
|
||||||
// auth: ['超级西(中成)药', 'sys:user:save'],
|
// icon: 'ix:export-check',
|
||||||
onClick: openExcelUploadModal.bind(null, 2),
|
// // auth: ['超级西(中成)药', 'sys:user:save'],
|
||||||
},
|
// onClick: openExcelUploadModal.bind(null, 2),
|
||||||
|
// },
|
||||||
{
|
{
|
||||||
label: '批量改价预览',
|
label: '批量改价预览',
|
||||||
type: 'primary',
|
type: 'primary',
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* 总仓 ExcelJS 导出
|
||||||
|
* - list:列表「导出」完整列(来自 export-data)
|
||||||
|
* - price:改价模板列(预览弹窗下载模板)
|
||||||
|
* 价格/单位一律文本原样写出,避免浮点改写
|
||||||
|
*/
|
||||||
import ExcelJS from 'exceljs';
|
import ExcelJS from 'exceljs';
|
||||||
|
|
||||||
import type { WarehouseBatchPriceItem } from '../api';
|
import type { WarehouseBatchPriceItem } from '../api';
|
||||||
@@ -9,33 +15,73 @@ const THIN_BORDER: Partial<ExcelJS.Borders> = {
|
|||||||
right: { style: 'thin', color: { argb: 'FF000000' } },
|
right: { style: 'thin', color: { argb: 'FF000000' } },
|
||||||
};
|
};
|
||||||
|
|
||||||
const HEADERS = [
|
/** export-data 行结构 */
|
||||||
|
export type WarehouseExportRow = {
|
||||||
|
drug_id?: number | string;
|
||||||
|
drug_number?: string;
|
||||||
|
drug_name?: string;
|
||||||
|
pinyin_simple?: string;
|
||||||
|
unit_name?: string;
|
||||||
|
type_text?: string;
|
||||||
|
status_text?: string;
|
||||||
|
market_price?: string | number;
|
||||||
|
price?: string | number;
|
||||||
|
created_at?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const LIST_HEADERS = [
|
||||||
'药品ID',
|
'药品ID',
|
||||||
'商品编码',
|
'商品编码',
|
||||||
'通用名',
|
'通用名',
|
||||||
'拼音首拼',
|
'拼音首拼',
|
||||||
|
'单位',
|
||||||
|
'类型',
|
||||||
|
'状态',
|
||||||
|
'每克供货价',
|
||||||
|
'每克零售价',
|
||||||
|
'创建时间',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const PRICE_HEADERS = [
|
||||||
|
'药品ID',
|
||||||
|
'商品编码',
|
||||||
|
'通用名',
|
||||||
|
'拼音首拼',
|
||||||
|
'单位',
|
||||||
'每克供货价',
|
'每克供货价',
|
||||||
'每克零售价',
|
'每克零售价',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export async function exportWarehousePriceExcel(
|
function priceToCellText(value: unknown): string {
|
||||||
items: WarehouseBatchPriceItem[],
|
if (value === null || value === undefined || value === '') {
|
||||||
): Promise<ArrayBuffer> {
|
return '';
|
||||||
const workbook = new ExcelJS.Workbook();
|
}
|
||||||
const sheet = workbook.addWorksheet('中药价格');
|
return String(value).trim();
|
||||||
|
}
|
||||||
|
|
||||||
sheet.columns = [
|
function resolveUnitName(item: { unit_name?: string; drug?: { unit?: { name?: string } } }): string {
|
||||||
{ width: 10 },
|
const direct = (item.unit_name ?? '').trim();
|
||||||
{ width: 14 },
|
if (direct) {
|
||||||
{ width: 18 },
|
return direct;
|
||||||
{ width: 12 },
|
}
|
||||||
{ width: 14 },
|
const nested = item.drug?.unit?.name;
|
||||||
{ width: 14 },
|
return nested != null ? String(nested).trim() : '';
|
||||||
];
|
}
|
||||||
|
|
||||||
const headerRow = sheet.addRow([...HEADERS]);
|
/**
|
||||||
headerRow.height = 28;
|
* 强制单元格为文本:ExcelJS addRow 遇「0.14」可能自动转 number,
|
||||||
headerRow.eachCell((cell) => {
|
* 再经 IEEE 回读会漂成 0.139999… / 0.90,造成假变更
|
||||||
|
*/
|
||||||
|
function forceTextCell(cell: ExcelJS.Cell, text: string) {
|
||||||
|
cell.value = text;
|
||||||
|
cell.numFmt = '@';
|
||||||
|
cell.border = THIN_BORDER;
|
||||||
|
cell.alignment = { horizontal: 'center', vertical: 'middle' };
|
||||||
|
}
|
||||||
|
|
||||||
|
function styleHeader(row: ExcelJS.Row) {
|
||||||
|
row.height = 28;
|
||||||
|
row.eachCell((cell) => {
|
||||||
cell.font = { bold: true };
|
cell.font = { bold: true };
|
||||||
cell.fill = {
|
cell.fill = {
|
||||||
type: 'pattern',
|
type: 'pattern',
|
||||||
@@ -45,38 +91,99 @@ export async function exportWarehousePriceExcel(
|
|||||||
cell.alignment = { horizontal: 'center', vertical: 'middle' };
|
cell.alignment = { horizontal: 'center', vertical: 'middle' };
|
||||||
cell.border = THIN_BORDER;
|
cell.border = THIN_BORDER;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
items.forEach((item, index) => {
|
function styleDataRowFill(row: ExcelJS.Row, index: number) {
|
||||||
const row = sheet.addRow([
|
if (index % 2 !== 1) {
|
||||||
item.drug_id,
|
return;
|
||||||
item.drug_number ?? '',
|
}
|
||||||
item.drug_name ?? '',
|
row.eachCell((cell) => {
|
||||||
item.pinyin_simple ?? '',
|
cell.fill = {
|
||||||
item.market_price ?? 0,
|
type: 'pattern',
|
||||||
item.price ?? 0,
|
pattern: 'solid',
|
||||||
]);
|
fgColor: { argb: 'FFF2F2F2' },
|
||||||
row.eachCell((cell, colNumber) => {
|
};
|
||||||
cell.border = THIN_BORDER;
|
|
||||||
cell.alignment = { horizontal: 'center', vertical: 'middle' };
|
|
||||||
if (colNumber === 2) {
|
|
||||||
cell.numFmt = '@';
|
|
||||||
} else if (colNumber >= 5) {
|
|
||||||
cell.numFmt = '#,##0.00';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (index % 2 === 1) {
|
|
||||||
row.eachCell((cell) => {
|
|
||||||
cell.fill = {
|
|
||||||
type: 'pattern',
|
|
||||||
pattern: 'solid',
|
|
||||||
fgColor: { argb: 'FFF2F2F2' },
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列表导出:export-data → ExcelJS
|
||||||
|
*/
|
||||||
|
export async function exportWarehouseListExcel(
|
||||||
|
rows: WarehouseExportRow[],
|
||||||
|
sheetName = '仓库商品',
|
||||||
|
): Promise<ArrayBuffer> {
|
||||||
|
const workbook = new ExcelJS.Workbook();
|
||||||
|
const sheet = workbook.addWorksheet(sheetName);
|
||||||
|
sheet.columns = [
|
||||||
|
{ width: 10 },
|
||||||
|
{ width: 14 },
|
||||||
|
{ width: 18 },
|
||||||
|
{ width: 12 },
|
||||||
|
{ width: 10 },
|
||||||
|
{ width: 12 },
|
||||||
|
{ width: 10 },
|
||||||
|
{ width: 14 },
|
||||||
|
{ width: 14 },
|
||||||
|
{ width: 18 },
|
||||||
|
];
|
||||||
|
styleHeader(sheet.addRow([...LIST_HEADERS]));
|
||||||
|
rows.forEach((item, index) => {
|
||||||
|
const row = sheet.addRow([]);
|
||||||
|
const values = [
|
||||||
|
String(item.drug_id ?? ''),
|
||||||
|
String(item.drug_number ?? ''),
|
||||||
|
String(item.drug_name ?? ''),
|
||||||
|
String(item.pinyin_simple ?? ''),
|
||||||
|
resolveUnitName(item),
|
||||||
|
String(item.type_text ?? ''),
|
||||||
|
String(item.status_text ?? ''),
|
||||||
|
priceToCellText(item.market_price),
|
||||||
|
priceToCellText(item.price),
|
||||||
|
String(item.created_at ?? ''),
|
||||||
|
];
|
||||||
|
values.forEach((text, i) => forceTextCell(row.getCell(i + 1), text));
|
||||||
|
styleDataRowFill(row, index);
|
||||||
|
});
|
||||||
|
sheet.views = [{ state: 'frozen', ySplit: 1 }];
|
||||||
|
const buffer = await workbook.xlsx.writeBuffer();
|
||||||
|
return buffer as ArrayBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 改价模板导出(预览弹窗)
|
||||||
|
*/
|
||||||
|
export async function exportWarehousePriceExcel(
|
||||||
|
items: WarehouseBatchPriceItem[] | WarehouseExportRow[],
|
||||||
|
sheetName = '仓库改价',
|
||||||
|
): Promise<ArrayBuffer> {
|
||||||
|
const workbook = new ExcelJS.Workbook();
|
||||||
|
const sheet = workbook.addWorksheet(sheetName);
|
||||||
|
sheet.columns = [
|
||||||
|
{ width: 10 },
|
||||||
|
{ width: 14 },
|
||||||
|
{ width: 18 },
|
||||||
|
{ width: 12 },
|
||||||
|
{ width: 10 },
|
||||||
|
{ width: 14 },
|
||||||
|
{ width: 14 },
|
||||||
|
];
|
||||||
|
styleHeader(sheet.addRow([...PRICE_HEADERS]));
|
||||||
|
items.forEach((item, index) => {
|
||||||
|
const row = sheet.addRow([]);
|
||||||
|
const values = [
|
||||||
|
String(item.drug_id ?? ''),
|
||||||
|
String(item.drug_number ?? ''),
|
||||||
|
String(item.drug_name ?? ''),
|
||||||
|
String(item.pinyin_simple ?? ''),
|
||||||
|
resolveUnitName(item),
|
||||||
|
priceToCellText(item.market_price),
|
||||||
|
priceToCellText(item.price),
|
||||||
|
];
|
||||||
|
values.forEach((text, i) => forceTextCell(row.getCell(i + 1), text));
|
||||||
|
styleDataRowFill(row, index);
|
||||||
|
});
|
||||||
sheet.views = [{ state: 'frozen', ySplit: 1 }];
|
sheet.views = [{ state: 'frozen', ySplit: 1 }];
|
||||||
|
|
||||||
const buffer = await workbook.xlsx.writeBuffer();
|
const buffer = await workbook.xlsx.writeBuffer();
|
||||||
return buffer as ArrayBuffer;
|
return buffer as ArrayBuffer;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,71 @@
|
|||||||
|
/**
|
||||||
|
* 总仓改价 Excel 解析
|
||||||
|
* - 解析药品ID,便于精确匹配
|
||||||
|
* - 价格优先读单元格「原始字符串 / 显示文本」,避免 IEEE 浮点
|
||||||
|
*/
|
||||||
import ExcelJS from 'exceljs';
|
import ExcelJS from 'exceljs';
|
||||||
|
|
||||||
import { formatPriceLikeBackend, parsePriceValue } from './priceCompare';
|
import { roundPrice4 } from './priceCompare';
|
||||||
|
|
||||||
const MAX_ROWS = 5000;
|
const MAX_ROWS = 5000;
|
||||||
|
|
||||||
export type ParsedWarehousePriceRow = {
|
export type ParsedWarehousePriceRow = {
|
||||||
|
drug_id: number;
|
||||||
drug_name: string;
|
drug_name: string;
|
||||||
drug_number: string;
|
drug_number: string;
|
||||||
market_price: number;
|
market_price: number;
|
||||||
price: number;
|
price: number;
|
||||||
|
unit_name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ParseWarehousePriceResult = {
|
export type ParseWarehousePriceResult = {
|
||||||
items: ParsedWarehousePriceRow[];
|
items: ParsedWarehousePriceRow[];
|
||||||
invalidCount: number;
|
invalidCount: number;
|
||||||
rowCount: number;
|
rowCount: number;
|
||||||
|
hasUnitColumn: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function normalizeHeader(value: string): string {
|
||||||
|
return value.replace(/\s+/g, '').replace(/\u3000/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function cellRawString(cell: ExcelJS.Cell): string {
|
||||||
|
const v = cell.value;
|
||||||
|
if (v === null || v === undefined) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
// 导出时我们写的是字符串,打开后若仍是字符串则最可信
|
||||||
|
if (typeof v === 'string') {
|
||||||
|
return v.trim().replace(/,/g, '').replace(/¥/g, '');
|
||||||
|
}
|
||||||
|
if (typeof v === 'number') {
|
||||||
|
// 不用直接 Number 展示;用足量有效位再交给 roundPrice4
|
||||||
|
return String(v);
|
||||||
|
}
|
||||||
|
if (typeof v === 'object') {
|
||||||
|
if ('result' in v && v.result !== null && v.result !== undefined) {
|
||||||
|
return String((v as { result: unknown }).result).trim();
|
||||||
|
}
|
||||||
|
if ('text' in v && (v as { text?: string }).text != null) {
|
||||||
|
return String((v as { text?: string }).text).trim();
|
||||||
|
}
|
||||||
|
if ('richText' in v && Array.isArray((v as { richText: { text: string }[] }).richText)) {
|
||||||
|
return (v as { richText: { text: string }[] }).richText
|
||||||
|
.map((p) => p.text)
|
||||||
|
.join('')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const display = String(cell.text ?? '').trim().replace(/,/g, '');
|
||||||
|
return display;
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeCell(value: ExcelJS.CellValue): string {
|
function normalizeCell(value: ExcelJS.CellValue): string {
|
||||||
if (value === null || value === undefined) {
|
if (value === null || value === undefined) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
if (typeof value === 'object' && 'text' in value) {
|
if (typeof value === 'object' && value && 'text' in value) {
|
||||||
return String(value.text ?? '').trim();
|
return String((value as { text?: string }).text ?? '').trim();
|
||||||
}
|
}
|
||||||
if (typeof value === 'number') {
|
if (typeof value === 'number') {
|
||||||
return Number.isInteger(value) ? String(value) : String(value).trim();
|
return Number.isInteger(value) ? String(value) : String(value).trim();
|
||||||
@@ -30,14 +73,12 @@ function normalizeCell(value: ExcelJS.CellValue): string {
|
|||||||
return String(value).trim();
|
return String(value).trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeHeader(value: string): string {
|
const DRUG_ID_ALIASES = ['药品ID', '药品Id', 'drug_id', 'DrugId'];
|
||||||
return value.replace(/\s+/g, '').replace(/\u3000/g, '');
|
const NUMBER_ALIASES = ['商品编码', '药品编码', 'ErpCode', 'drug_number'];
|
||||||
}
|
|
||||||
|
|
||||||
const NUMBER_ALIASES = ['商品编码', '药品编码', '编码', 'drug_number'];
|
|
||||||
const NAME_ALIASES = ['通用名', '药品名称', '药名', 'drug_name'];
|
const NAME_ALIASES = ['通用名', '药品名称', '药名', 'drug_name'];
|
||||||
const MARKET_ALIASES = ['每克供货价', '供货价', 'market_price'];
|
const MARKET_ALIASES = ['每克供货价', '供货价', 'market_price'];
|
||||||
const PRICE_ALIASES = ['每克零售价', '建议售价', '零售价', 'price'];
|
const PRICE_ALIASES = ['每克零售价', '建议售价', '零售价', '当前零售价', 'price'];
|
||||||
|
const UNIT_ALIASES = ['单位', '计量单位', 'unit', 'unit_name'];
|
||||||
|
|
||||||
function buildHeaderIndexMap(headerRow: ExcelJS.Row): Record<string, number> {
|
function buildHeaderIndexMap(headerRow: ExcelJS.Row): Record<string, number> {
|
||||||
const map: Record<string, number> = {};
|
const map: Record<string, number> = {};
|
||||||
@@ -67,14 +108,23 @@ function getCellNumber(row: ExcelJS.Row, col?: number): number {
|
|||||||
if (!col) {
|
if (!col) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return parsePriceValue(row.getCell(col).value);
|
const raw = cellRawString(row.getCell(col));
|
||||||
|
if (raw === '' || !Number.isFinite(Number(raw))) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return roundPrice4(raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCellText(row: ExcelJS.Row, col?: number): string {
|
function getCellText(row: ExcelJS.Row, col?: number): string {
|
||||||
if (!col) {
|
if (!col) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
return normalizeCell(row.getCell(col).value);
|
const cell = row.getCell(col);
|
||||||
|
const fromValue = normalizeCell(cell.value);
|
||||||
|
if (fromValue) {
|
||||||
|
return fromValue;
|
||||||
|
}
|
||||||
|
return String(cell.text ?? '').trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
function rowIsEmpty(row: ExcelJS.Row): boolean {
|
function rowIsEmpty(row: ExcelJS.Row): boolean {
|
||||||
@@ -95,16 +145,19 @@ export async function parseWarehousePriceExcelBuffer(
|
|||||||
|
|
||||||
const sheet = workbook.worksheets[0];
|
const sheet = workbook.worksheets[0];
|
||||||
if (!sheet) {
|
if (!sheet) {
|
||||||
return { items: [], invalidCount: 0, rowCount: 0 };
|
return { items: [], invalidCount: 0, rowCount: 0, hasUnitColumn: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
const headerRow = sheet.getRow(1);
|
const headerRow = sheet.getRow(1);
|
||||||
const headerMap = buildHeaderIndexMap(headerRow);
|
const headerMap = buildHeaderIndexMap(headerRow);
|
||||||
|
|
||||||
|
const drugIdCol = pickColumn(headerMap, DRUG_ID_ALIASES);
|
||||||
const numberCol = pickColumn(headerMap, NUMBER_ALIASES);
|
const numberCol = pickColumn(headerMap, NUMBER_ALIASES);
|
||||||
const nameCol = pickColumn(headerMap, NAME_ALIASES);
|
const nameCol = pickColumn(headerMap, NAME_ALIASES);
|
||||||
const marketCol = pickColumn(headerMap, MARKET_ALIASES);
|
const marketCol = pickColumn(headerMap, MARKET_ALIASES);
|
||||||
const priceCol = pickColumn(headerMap, PRICE_ALIASES);
|
const priceCol = pickColumn(headerMap, PRICE_ALIASES);
|
||||||
|
const unitCol = pickColumn(headerMap, UNIT_ALIASES);
|
||||||
|
const hasUnitColumn = !!unitCol;
|
||||||
|
|
||||||
const items: ParsedWarehousePriceRow[] = [];
|
const items: ParsedWarehousePriceRow[] = [];
|
||||||
let invalidCount = 0;
|
let invalidCount = 0;
|
||||||
@@ -116,25 +169,37 @@ export async function parseWarehousePriceExcelBuffer(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const drugIdRaw = getCellText(row, drugIdCol);
|
||||||
|
const drugId = Number.parseInt(drugIdRaw, 10) || 0;
|
||||||
const drugNumber = getCellText(row, numberCol);
|
const drugNumber = getCellText(row, numberCol);
|
||||||
const drugName = getCellText(row, nameCol);
|
const drugName = getCellText(row, nameCol);
|
||||||
const marketPrice = getCellNumber(row, marketCol);
|
const marketPrice = getCellNumber(row, marketCol);
|
||||||
const price = getCellNumber(row, priceCol);
|
const price = getCellNumber(row, priceCol);
|
||||||
|
const unitName = getCellText(row, unitCol);
|
||||||
|
|
||||||
if (!drugName && !drugNumber && marketPrice === 0 && price === 0) {
|
if (
|
||||||
|
drugId <= 0 &&
|
||||||
|
!drugName &&
|
||||||
|
!drugNumber &&
|
||||||
|
marketPrice === 0 &&
|
||||||
|
price === 0 &&
|
||||||
|
!unitName
|
||||||
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!drugName && !drugNumber) {
|
if (drugId <= 0 && !drugName && !drugNumber) {
|
||||||
invalidCount++;
|
invalidCount++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
items.push({
|
items.push({
|
||||||
|
drug_id: drugId,
|
||||||
drug_name: drugName,
|
drug_name: drugName,
|
||||||
drug_number: drugNumber,
|
drug_number: drugNumber,
|
||||||
market_price: formatPriceLikeBackend(marketPrice),
|
market_price: marketPrice,
|
||||||
price: formatPriceLikeBackend(price),
|
price,
|
||||||
|
unit_name: unitName,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,5 +207,6 @@ export async function parseWarehousePriceExcelBuffer(
|
|||||||
items,
|
items,
|
||||||
invalidCount,
|
invalidCount,
|
||||||
rowCount: Math.max(0, rowCount - 1),
|
rowCount: Math.max(0, rowCount - 1),
|
||||||
|
hasUnitColumn,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* 仓库改价对比工具
|
||||||
|
* 库内价格 decimal(10,4)。预览对比禁止 format_price。
|
||||||
|
* Excel IEEE 浮点常见把 0.91 读成 ≈0.8999→0.90,需在合并时与快照对账。
|
||||||
|
*/
|
||||||
export type PricePair = {
|
export type PricePair = {
|
||||||
market_price: number;
|
market_price: number;
|
||||||
price: number;
|
price: number;
|
||||||
|
unit_name?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function parsePriceValue(value: unknown): number {
|
export function parsePriceValue(value: unknown): number {
|
||||||
if (value === null || value === undefined || value === '') {
|
if (value === null || value === undefined || value === '') {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const cleaned = value.trim().replace(/,/g, '').replace(/¥/g, '');
|
||||||
|
if (cleaned === '') {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const num = Number(cleaned);
|
||||||
|
return Number.isFinite(num) ? num : 0;
|
||||||
|
}
|
||||||
const num = Number(value);
|
const num = Number(value);
|
||||||
return Number.isFinite(num) ? num : 0;
|
return Number.isFinite(num) ? num : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 对齐库字段 decimal(10,4) */
|
||||||
|
export function roundPrice4(value: unknown): number {
|
||||||
|
return Math.round(parsePriceValue(value) * 10000) / 10000;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 与后端 format_price 一致:第三位小数非 0 则分位进 1,再保留两位小数。
|
* Excel 读入价与快照对账:未改到「分」或仅 1 分浮点漂移时,强制用快照价
|
||||||
|
* 避免 0.91→0.90、0.09→0.08 这类误报
|
||||||
|
*/
|
||||||
|
export function reconcileExcelPrice(excel: unknown, snapshot: unknown): number {
|
||||||
|
const e = roundPrice4(excel);
|
||||||
|
const s = roundPrice4(snapshot);
|
||||||
|
if (e === s) {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
const fenE = Math.round(e * 100);
|
||||||
|
const fenS = Math.round(s * 100);
|
||||||
|
// 分位相同 → 未改价
|
||||||
|
if (fenE === fenS) {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
// 仅差 1 分且更像浮点下漂/上漂,视为未改(用户真改 1 分极少见于每克价误触)
|
||||||
|
if (Math.abs(fenE - fenS) === 1 && Math.abs(e - s) <= 0.011) {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 与后端 format_price 一致:仅确认落库时用
|
||||||
*/
|
*/
|
||||||
export function formatPriceLikeBackend(value: unknown): number {
|
export function formatPriceLikeBackend(value: unknown): number {
|
||||||
const price = parsePriceValue(value);
|
const price = parsePriceValue(value);
|
||||||
@@ -26,17 +68,20 @@ export function formatPriceLikeBackend(value: unknown): number {
|
|||||||
|
|
||||||
export function normalizePricePair(pair: PricePair): PricePair {
|
export function normalizePricePair(pair: PricePair): PricePair {
|
||||||
return {
|
return {
|
||||||
market_price: formatPriceLikeBackend(pair.market_price),
|
market_price: roundPrice4(pair.market_price),
|
||||||
price: formatPriceLikeBackend(pair.price),
|
price: roundPrice4(pair.price),
|
||||||
|
unit_name: (pair.unit_name ?? '').trim(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toPriceUnits(value: unknown): number {
|
export function toPriceUnits(value: unknown): number {
|
||||||
return Math.round(formatPriceLikeBackend(value) * 100);
|
return Math.round(roundPrice4(value) * 10000);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatPriceDisplay(value: unknown): string {
|
export function formatPriceDisplay(value: unknown): string {
|
||||||
return formatPriceLikeBackend(value).toFixed(2);
|
const n = roundPrice4(value);
|
||||||
|
const fixed = n.toFixed(4);
|
||||||
|
return fixed.replace(/\.?0+$/, '') || '0';
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PriceDelta = 'up' | 'down' | 'same';
|
export type PriceDelta = 'up' | 'down' | 'same';
|
||||||
@@ -57,6 +102,13 @@ export function isFieldChanged(orig: number, next: number): boolean {
|
|||||||
return getPriceDelta(orig, next) !== 'same';
|
return getPriceDelta(orig, next) !== 'same';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isUnitChanged(
|
||||||
|
orig: string | undefined,
|
||||||
|
next: string | undefined,
|
||||||
|
): boolean {
|
||||||
|
return (orig ?? '').trim() !== (next ?? '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
export function isPriceChanged(
|
export function isPriceChanged(
|
||||||
initial: PricePair | undefined,
|
initial: PricePair | undefined,
|
||||||
current: PricePair,
|
current: PricePair,
|
||||||
@@ -66,7 +118,8 @@ export function isPriceChanged(
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
isFieldChanged(initial.market_price, current.market_price) ||
|
isFieldChanged(initial.market_price, current.market_price) ||
|
||||||
isFieldChanged(initial.price, current.price)
|
isFieldChanged(initial.price, current.price) ||
|
||||||
|
isUnitChanged(initial.unit_name, current.unit_name)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
isFieldChanged,
|
isFieldChanged,
|
||||||
isMarketGtPrice,
|
isMarketGtPrice,
|
||||||
isPriceChanged,
|
isPriceChanged,
|
||||||
|
isUnitChanged,
|
||||||
type PriceDelta,
|
type PriceDelta,
|
||||||
type PricePair,
|
type PricePair,
|
||||||
} from '../../admin/utils/priceCompare';
|
} from '../../admin/utils/priceCompare';
|
||||||
@@ -18,6 +19,8 @@ export type PriceCompareFilterTab =
|
|||||||
| 'changed'
|
| 'changed'
|
||||||
| 'unchanged'
|
| 'unchanged'
|
||||||
| 'marketGtPrice'
|
| 'marketGtPrice'
|
||||||
|
| 'unitChanged'
|
||||||
|
| 'priceLocked'
|
||||||
| 'new'
|
| 'new'
|
||||||
| 'unmatched';
|
| 'unmatched';
|
||||||
|
|
||||||
@@ -29,12 +32,19 @@ export type PriceCompareRow = {
|
|||||||
pinyin_simple?: string;
|
pinyin_simple?: string;
|
||||||
orig_market_price: number;
|
orig_market_price: number;
|
||||||
orig_price: number;
|
orig_price: number;
|
||||||
|
orig_unit_name?: string;
|
||||||
market_price: number;
|
market_price: number;
|
||||||
price: number;
|
price: number;
|
||||||
|
unit_name?: string;
|
||||||
_changed: boolean;
|
_changed: boolean;
|
||||||
_marketGtPrice: boolean;
|
_marketGtPrice: boolean;
|
||||||
|
_unitChanged?: boolean;
|
||||||
|
/** 单位名不在字典中,确认时将自动新建 */
|
||||||
|
_unitPendingCreate?: boolean;
|
||||||
_isNew: boolean;
|
_isNew: boolean;
|
||||||
_fromExcel?: boolean;
|
_fromExcel?: boolean;
|
||||||
|
/** 配送仓锁定,不可改价(门店) */
|
||||||
|
_priceLocked?: boolean;
|
||||||
_marketDelta: PriceDelta;
|
_marketDelta: PriceDelta;
|
||||||
_priceDelta: PriceDelta;
|
_priceDelta: PriceDelta;
|
||||||
_marketChanged: boolean;
|
_marketChanged: boolean;
|
||||||
@@ -46,6 +56,7 @@ export type UnmatchedRow = {
|
|||||||
drug_number: string;
|
drug_number: string;
|
||||||
market_price: number;
|
market_price: number;
|
||||||
price: number;
|
price: number;
|
||||||
|
unit_name?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
@@ -54,11 +65,26 @@ const props = withDefaults(
|
|||||||
unmatchedRows?: UnmatchedRow[];
|
unmatchedRows?: UnmatchedRow[];
|
||||||
filterTab?: PriceCompareFilterTab;
|
filterTab?: PriceCompareFilterTab;
|
||||||
savingRowId?: number | null;
|
savingRowId?: number | null;
|
||||||
|
/** 是否展示单位编辑(平台 true,门店 false) */
|
||||||
|
showUnit?: boolean;
|
||||||
|
/** 是否展示供货价(门店仅零售价时为 false) */
|
||||||
|
showMarketPrice?: boolean;
|
||||||
|
/** 售价列文案 */
|
||||||
|
priceLabel?: string;
|
||||||
|
/** 是否显示单行保存按钮 */
|
||||||
|
showRowSave?: boolean;
|
||||||
|
/** 是否展示「不可改价」筛选 Tab */
|
||||||
|
showPriceLockedTab?: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
unmatchedRows: () => [],
|
unmatchedRows: () => [],
|
||||||
filterTab: 'all',
|
filterTab: 'all',
|
||||||
savingRowId: null,
|
savingRowId: null,
|
||||||
|
showUnit: false,
|
||||||
|
showMarketPrice: true,
|
||||||
|
priceLabel: '售价',
|
||||||
|
showRowSave: true,
|
||||||
|
showPriceLockedTab: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -84,12 +110,56 @@ watch(innerFilterTab, (v) => {
|
|||||||
|
|
||||||
const tabCounts = computed(() => {
|
const tabCounts = computed(() => {
|
||||||
const all = props.rows.length;
|
const all = props.rows.length;
|
||||||
const changed = props.rows.filter((r) => r._changed).length;
|
const changed = props.rows.filter((r) => r._changed && !r._priceLocked).length;
|
||||||
const unchanged = all - changed;
|
const unchanged = props.rows.filter(
|
||||||
|
(r) => !r._changed && !r._priceLocked && !r._isNew,
|
||||||
|
).length;
|
||||||
const marketGtPrice = props.rows.filter((r) => r._marketGtPrice).length;
|
const marketGtPrice = props.rows.filter((r) => r._marketGtPrice).length;
|
||||||
|
const unitChanged = props.rows.filter((r) => r._unitChanged).length;
|
||||||
|
const priceLocked = props.rows.filter((r) => r._priceLocked).length;
|
||||||
const newCount = props.rows.filter((r) => r._isNew).length;
|
const newCount = props.rows.filter((r) => r._isNew).length;
|
||||||
const unmatched = props.unmatchedRows.length;
|
const unmatched = props.unmatchedRows.length;
|
||||||
return { all, changed, unchanged, marketGtPrice, new: newCount, unmatched };
|
return {
|
||||||
|
all,
|
||||||
|
changed,
|
||||||
|
unchanged,
|
||||||
|
marketGtPrice,
|
||||||
|
unitChanged,
|
||||||
|
priceLocked,
|
||||||
|
new: newCount,
|
||||||
|
unmatched,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const filterOptions = computed(() => {
|
||||||
|
const opts: { label: string; value: PriceCompareFilterTab }[] = [
|
||||||
|
{ label: `全部 ${tabCounts.value.all}`, value: 'all' },
|
||||||
|
{ label: `有修改 ${tabCounts.value.changed}`, value: 'changed' },
|
||||||
|
{ label: `无修改 ${tabCounts.value.unchanged}`, value: 'unchanged' },
|
||||||
|
];
|
||||||
|
if (props.showMarketPrice) {
|
||||||
|
opts.push({
|
||||||
|
label: `供价超售价 ${tabCounts.value.marketGtPrice}`,
|
||||||
|
value: 'marketGtPrice',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (props.showUnit) {
|
||||||
|
opts.push({
|
||||||
|
label: `单位变更 ${tabCounts.value.unitChanged}`,
|
||||||
|
value: 'unitChanged',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (props.showPriceLockedTab) {
|
||||||
|
opts.push({
|
||||||
|
label: `不可改价 ${tabCounts.value.priceLocked}`,
|
||||||
|
value: 'priceLocked',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
opts.push(
|
||||||
|
{ label: `待新增 ${tabCounts.value.new}`, value: 'new' },
|
||||||
|
{ label: `未匹配 ${tabCounts.value.unmatched}`, value: 'unmatched' },
|
||||||
|
);
|
||||||
|
return opts;
|
||||||
});
|
});
|
||||||
|
|
||||||
const filteredRows = computed(() => {
|
const filteredRows = computed(() => {
|
||||||
@@ -97,11 +167,15 @@ const filteredRows = computed(() => {
|
|||||||
let list = props.rows;
|
let list = props.rows;
|
||||||
|
|
||||||
if (innerFilterTab.value === 'changed') {
|
if (innerFilterTab.value === 'changed') {
|
||||||
list = list.filter((r) => r._changed);
|
list = list.filter((r) => r._changed && !r._priceLocked);
|
||||||
} else if (innerFilterTab.value === 'unchanged') {
|
} else if (innerFilterTab.value === 'unchanged') {
|
||||||
list = list.filter((r) => !r._changed);
|
list = list.filter((r) => !r._changed && !r._priceLocked && !r._isNew);
|
||||||
} else if (innerFilterTab.value === 'marketGtPrice') {
|
} else if (innerFilterTab.value === 'marketGtPrice') {
|
||||||
list = list.filter((r) => r._marketGtPrice);
|
list = list.filter((r) => r._marketGtPrice);
|
||||||
|
} else if (innerFilterTab.value === 'unitChanged') {
|
||||||
|
list = list.filter((r) => r._unitChanged);
|
||||||
|
} else if (innerFilterTab.value === 'priceLocked') {
|
||||||
|
list = list.filter((r) => r._priceLocked);
|
||||||
} else if (innerFilterTab.value === 'new') {
|
} else if (innerFilterTab.value === 'new') {
|
||||||
list = list.filter((r) => r._isNew);
|
list = list.filter((r) => r._isNew);
|
||||||
}
|
}
|
||||||
@@ -139,7 +213,11 @@ function cardClass(row: PriceCompareRow): string[] {
|
|||||||
const classes = [
|
const classes = [
|
||||||
'relative group rounded-xl border p-3 shadow-sm transition-all duration-300 ease-out hover:-translate-y-1 hover:shadow-md dark:bg-gray-800/80',
|
'relative group rounded-xl border p-3 shadow-sm transition-all duration-300 ease-out hover:-translate-y-1 hover:shadow-md dark:bg-gray-800/80',
|
||||||
];
|
];
|
||||||
if (row._isNew) {
|
if (row._priceLocked) {
|
||||||
|
classes.push(
|
||||||
|
'border-orange-200 bg-orange-50/40 dark:border-orange-500/30 dark:bg-orange-900/10',
|
||||||
|
);
|
||||||
|
} else if (row._isNew) {
|
||||||
classes.push('border-blue-200 bg-blue-50/30 dark:border-blue-500/30 dark:bg-blue-900/10');
|
classes.push('border-blue-200 bg-blue-50/30 dark:border-blue-500/30 dark:bg-blue-900/10');
|
||||||
} else if (row._marketGtPrice) {
|
} else if (row._marketGtPrice) {
|
||||||
classes.push(
|
classes.push(
|
||||||
@@ -159,10 +237,12 @@ function refreshRowFlags(row: PriceCompareRow) {
|
|||||||
const snapshot: PricePair = {
|
const snapshot: PricePair = {
|
||||||
market_price: row.orig_market_price,
|
market_price: row.orig_market_price,
|
||||||
price: row.orig_price,
|
price: row.orig_price,
|
||||||
|
unit_name: row.orig_unit_name,
|
||||||
};
|
};
|
||||||
const current: PricePair = {
|
const current: PricePair = {
|
||||||
market_price: row.market_price,
|
market_price: row.market_price,
|
||||||
price: row.price,
|
price: row.price,
|
||||||
|
unit_name: row.unit_name,
|
||||||
};
|
};
|
||||||
row._marketDelta = getPriceDelta(snapshot.market_price, current.market_price);
|
row._marketDelta = getPriceDelta(snapshot.market_price, current.market_price);
|
||||||
row._priceDelta = getPriceDelta(snapshot.price, current.price);
|
row._priceDelta = getPriceDelta(snapshot.price, current.price);
|
||||||
@@ -171,6 +251,7 @@ function refreshRowFlags(row: PriceCompareRow) {
|
|||||||
current.market_price,
|
current.market_price,
|
||||||
);
|
);
|
||||||
row._priceChanged = isFieldChanged(snapshot.price, current.price);
|
row._priceChanged = isFieldChanged(snapshot.price, current.price);
|
||||||
|
row._unitChanged = isUnitChanged(snapshot.unit_name, current.unit_name);
|
||||||
row._changed = row._isNew || isPriceChanged(snapshot, current);
|
row._changed = row._isNew || isPriceChanged(snapshot, current);
|
||||||
row._marketGtPrice = isMarketGtPrice(current);
|
row._marketGtPrice = isMarketGtPrice(current);
|
||||||
}
|
}
|
||||||
@@ -185,6 +266,12 @@ function onPriceChange(
|
|||||||
emit('cellChange', { ...row });
|
emit('cellChange', { ...row });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onUnitChange(row: PriceCompareRow, value: string) {
|
||||||
|
row.unit_name = value;
|
||||||
|
refreshRowFlags(row);
|
||||||
|
emit('cellChange', { ...row });
|
||||||
|
}
|
||||||
|
|
||||||
function handleRowSave(row: PriceCompareRow) {
|
function handleRowSave(row: PriceCompareRow) {
|
||||||
emit('rowSave', row);
|
emit('rowSave', row);
|
||||||
}
|
}
|
||||||
@@ -209,17 +296,7 @@ function handleRowSave(row: PriceCompareRow) {
|
|||||||
<Segmented
|
<Segmented
|
||||||
v-model:value="innerFilterTab"
|
v-model:value="innerFilterTab"
|
||||||
class="mb-4 flex-shrink-0 flex-wrap !rounded-lg !p-1 shadow-sm"
|
class="mb-4 flex-shrink-0 flex-wrap !rounded-lg !p-1 shadow-sm"
|
||||||
:options="[
|
:options="filterOptions"
|
||||||
{ label: `全部 ${tabCounts.all}`, value: 'all' },
|
|
||||||
{ label: `有修改 ${tabCounts.changed}`, value: 'changed' },
|
|
||||||
{ label: `无修改 ${tabCounts.unchanged}`, value: 'unchanged' },
|
|
||||||
{
|
|
||||||
label: `供价超售价 ${tabCounts.marketGtPrice}`,
|
|
||||||
value: 'marketGtPrice',
|
|
||||||
},
|
|
||||||
{ label: `待新增 ${tabCounts.new}`, value: 'new' },
|
|
||||||
{ label: `未匹配 ${tabCounts.unmatched}`, value: 'unmatched' },
|
|
||||||
]"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Main Content Area -->
|
<!-- Main Content Area -->
|
||||||
@@ -264,14 +341,30 @@ function handleRowSave(row: PriceCompareRow) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Status Tags -->
|
<!-- Status Tags -->
|
||||||
<div v-if="row._isNew" class="mb-2">
|
<div v-if="row._isNew || row._priceLocked" class="mb-2 flex flex-wrap gap-1">
|
||||||
<Tag color="blue" class="!mr-0 !border-blue-200 !text-[11px] shadow-sm">待新增</Tag>
|
<Tag
|
||||||
|
v-if="row._isNew"
|
||||||
|
color="blue"
|
||||||
|
class="!mr-0 !border-blue-200 !text-[11px] shadow-sm"
|
||||||
|
>
|
||||||
|
待新增
|
||||||
|
</Tag>
|
||||||
|
<Tag
|
||||||
|
v-if="row._priceLocked"
|
||||||
|
color="orange"
|
||||||
|
class="!mr-0 !text-[11px] shadow-sm"
|
||||||
|
>
|
||||||
|
不可改价
|
||||||
|
</Tag>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Price Inputs Area -->
|
<!-- Price Inputs Area -->
|
||||||
<div class="space-y-2.5 text-xs">
|
<div class="space-y-2.5 text-xs">
|
||||||
<!-- Market Price Row -->
|
<!-- Market Price Row -->
|
||||||
<div class="flex items-center justify-between group/row">
|
<div
|
||||||
|
v-if="showMarketPrice"
|
||||||
|
class="flex items-center justify-between group/row"
|
||||||
|
>
|
||||||
<span class="w-12 flex-shrink-0 font-medium text-gray-500 dark:text-gray-400">
|
<span class="w-12 flex-shrink-0 font-medium text-gray-500 dark:text-gray-400">
|
||||||
供货价
|
供货价
|
||||||
</span>
|
</span>
|
||||||
@@ -288,6 +381,7 @@ function handleRowSave(row: PriceCompareRow) {
|
|||||||
:precision="4"
|
:precision="4"
|
||||||
:step="0.0001"
|
:step="0.0001"
|
||||||
size="small"
|
size="small"
|
||||||
|
:disabled="!!row._priceLocked"
|
||||||
:value="row.market_price"
|
:value="row.market_price"
|
||||||
@update:value="
|
@update:value="
|
||||||
(v) => onPriceChange(row, 'market_price', v as number)
|
(v) => onPriceChange(row, 'market_price', v as number)
|
||||||
@@ -307,7 +401,7 @@ function handleRowSave(row: PriceCompareRow) {
|
|||||||
<!-- Sell Price Row -->
|
<!-- Sell Price Row -->
|
||||||
<div class="flex items-center justify-between group/row">
|
<div class="flex items-center justify-between group/row">
|
||||||
<span class="w-12 flex-shrink-0 font-medium text-gray-500 dark:text-gray-400">
|
<span class="w-12 flex-shrink-0 font-medium text-gray-500 dark:text-gray-400">
|
||||||
售价
|
{{ priceLabel }}
|
||||||
</span>
|
</span>
|
||||||
<div class="flex flex-1 items-center justify-end gap-2">
|
<div class="flex flex-1 items-center justify-end gap-2">
|
||||||
<template v-if="!row._isNew">
|
<template v-if="!row._isNew">
|
||||||
@@ -322,6 +416,7 @@ function handleRowSave(row: PriceCompareRow) {
|
|||||||
:precision="4"
|
:precision="4"
|
||||||
:step="0.0001"
|
:step="0.0001"
|
||||||
size="small"
|
size="small"
|
||||||
|
:disabled="!!row._priceLocked"
|
||||||
:value="row.price"
|
:value="row.price"
|
||||||
@update:value="
|
@update:value="
|
||||||
(v) => onPriceChange(row, 'price', v as number)
|
(v) => onPriceChange(row, 'price', v as number)
|
||||||
@@ -337,6 +432,47 @@ function handleRowSave(row: PriceCompareRow) {
|
|||||||
<span v-else class="w-4"></span>
|
<span v-else class="w-4"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Unit Row(平台) -->
|
||||||
|
<div
|
||||||
|
v-if="showUnit"
|
||||||
|
class="flex items-center justify-between group/row"
|
||||||
|
>
|
||||||
|
<span class="w-12 flex-shrink-0 font-medium text-gray-500 dark:text-gray-400">
|
||||||
|
单位
|
||||||
|
</span>
|
||||||
|
<div class="flex flex-1 items-center justify-end gap-2">
|
||||||
|
<template v-if="!row._isNew">
|
||||||
|
<span class="text-gray-400 line-through decoration-gray-300 dark:text-gray-500">
|
||||||
|
{{ row.orig_unit_name || '—' }}
|
||||||
|
</span>
|
||||||
|
<span class="text-gray-300 dark:text-gray-600">→</span>
|
||||||
|
</template>
|
||||||
|
<Input
|
||||||
|
class="!w-[85px] !rounded-md shadow-sm"
|
||||||
|
size="small"
|
||||||
|
:value="row.unit_name"
|
||||||
|
placeholder="单位"
|
||||||
|
@update:value="(v) => onUnitChange(row, String(v ?? ''))"
|
||||||
|
/>
|
||||||
|
<Tag
|
||||||
|
v-if="row._unitPendingCreate"
|
||||||
|
color="orange"
|
||||||
|
class="!mr-0 !text-[10px]"
|
||||||
|
>
|
||||||
|
待建
|
||||||
|
</Tag>
|
||||||
|
<span v-else class="w-4"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 门店只读单位对照 -->
|
||||||
|
<div
|
||||||
|
v-else-if="row.unit_name || row.orig_unit_name"
|
||||||
|
class="flex items-center justify-between text-gray-400"
|
||||||
|
>
|
||||||
|
<span class="w-12 flex-shrink-0">单位</span>
|
||||||
|
<span>{{ row.unit_name || row.orig_unit_name || '—' }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Action Area -->
|
<!-- Action Area -->
|
||||||
@@ -345,7 +481,7 @@ function handleRowSave(row: PriceCompareRow) {
|
|||||||
>
|
>
|
||||||
<Transition name="slide-up">
|
<Transition name="slide-up">
|
||||||
<Button
|
<Button
|
||||||
v-if="row._changed"
|
v-if="showRowSave && row._changed && !row._priceLocked"
|
||||||
:loading="savingRowId === row.id"
|
:loading="savingRowId === row.id"
|
||||||
size="small"
|
size="small"
|
||||||
type="primary"
|
type="primary"
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
/**
|
||||||
|
* 门店仓库药品编辑弹窗
|
||||||
|
* 绑定配送仓不可改价;有底价/最高价时提示改价区间
|
||||||
|
*/
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
import { useVbenModal } from '@vben/common-ui';
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
import { useUserStore } from '@vben/stores';
|
|
||||||
|
|
||||||
import { message } from 'ant-design-vue';
|
import { Alert, message } from 'ant-design-vue';
|
||||||
|
|
||||||
import { useVbenForm } from '#/adapter/form';
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
|
||||||
@@ -13,10 +16,7 @@ import {
|
|||||||
updateWarehouseDrugManagementStore,
|
updateWarehouseDrugManagementStore,
|
||||||
} from '../api';
|
} from '../api';
|
||||||
import { modalFormProps } from '../config/form';
|
import { modalFormProps } from '../config/form';
|
||||||
import {getDrugUseList} from "#/views/doctor/doctor-reception/api";
|
import { getDrugUseList } from '#/views/doctor/doctor-reception/api';
|
||||||
|
|
||||||
|
|
||||||
const userStore = useUserStore();
|
|
||||||
|
|
||||||
const drugTime = ref([]);
|
const drugTime = ref([]);
|
||||||
const drugType = ref([]);
|
const drugType = ref([]);
|
||||||
@@ -52,10 +52,13 @@ getDrugUseList().then((res) => {
|
|||||||
|
|
||||||
const isUpdate = ref(false);
|
const isUpdate = ref(false);
|
||||||
const gridApi = ref();
|
const gridApi = ref();
|
||||||
|
/** 是否绑定配送仓(不可改价) */
|
||||||
|
const priceLocked = ref(false);
|
||||||
|
/** 改价区间展示文案 */
|
||||||
|
const priceRangeTip = ref('');
|
||||||
|
|
||||||
const [Form, formApi] = useVbenForm(modalFormProps);
|
const [Form, formApi] = useVbenForm(modalFormProps);
|
||||||
|
|
||||||
|
|
||||||
const [Modal, modalApi] = useVbenModal({
|
const [Modal, modalApi] = useVbenModal({
|
||||||
fullscreenButton: false,
|
fullscreenButton: false,
|
||||||
draggable: true,
|
draggable: true,
|
||||||
@@ -63,6 +66,10 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
modalApi.close();
|
modalApi.close();
|
||||||
},
|
},
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
|
if (priceLocked.value) {
|
||||||
|
message.warning('该商品不可改价');
|
||||||
|
return;
|
||||||
|
}
|
||||||
formApi.validate().then(async (e: any) => {
|
formApi.validate().then(async (e: any) => {
|
||||||
if (e.valid) {
|
if (e.valid) {
|
||||||
const values = await formApi.getValues();
|
const values = await formApi.getValues();
|
||||||
@@ -86,7 +93,6 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
onOpenChange(isOpen: boolean) {
|
onOpenChange(isOpen: boolean) {
|
||||||
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
|
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
|
|
||||||
formApi.updateSchema([
|
formApi.updateSchema([
|
||||||
{
|
{
|
||||||
componentProps: {
|
componentProps: {
|
||||||
@@ -115,19 +121,61 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
]);
|
]);
|
||||||
const { values, update } = modalApi.getData<Record<string, any>>();
|
const { values, update } = modalApi.getData<Record<string, any>>();
|
||||||
if (values) {
|
if (values) {
|
||||||
console.log(values, 'sssssssssssss')
|
|
||||||
isUpdate.value = update;
|
isUpdate.value = update;
|
||||||
formApi.setValues(values);
|
// 从列表行带入配送仓标记与价格区间(drug 嵌套字段展平到表单)
|
||||||
|
const hasDelivery =
|
||||||
|
Number(values.has_delivery_warehouse) === 1 ||
|
||||||
|
Number(values.drug?.has_delivery_warehouse) === 1;
|
||||||
|
priceLocked.value = hasDelivery;
|
||||||
|
const min = Number(
|
||||||
|
values.min_adjust_price ?? values.drug?.min_adjust_price ?? 0,
|
||||||
|
);
|
||||||
|
const max = Number(
|
||||||
|
values.max_adjust_price ?? values.drug?.max_adjust_price ?? 0,
|
||||||
|
);
|
||||||
|
if (min > 0 || max > 0) {
|
||||||
|
const minTxt = min > 0 ? `¥${min.toFixed(2)}` : '不限';
|
||||||
|
const maxTxt = max > 0 ? `¥${max.toFixed(2)}` : '不限';
|
||||||
|
priceRangeTip.value = `改价区间:${minTxt} ~ ${maxTxt}`;
|
||||||
|
} else {
|
||||||
|
priceRangeTip.value = '';
|
||||||
|
}
|
||||||
|
formApi.setValues({
|
||||||
|
...values,
|
||||||
|
has_delivery_warehouse: hasDelivery ? 1 : 0,
|
||||||
|
min_adjust_price: min || null,
|
||||||
|
max_adjust_price: max || null,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
priceLocked.value = false;
|
||||||
|
priceRangeTip.value = '';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const modalTitle = computed(() =>
|
||||||
|
isUpdate.value === true ? '编辑仓库药品' : '新增仓库药品',
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<Modal
|
<Modal :title="modalTitle" class="w-[80%] md:w-[50%] lg:w-[30%] h-[60%]">
|
||||||
:title="`${isUpdate === true ? '编辑' : '新增'}仓库药品`"
|
<Alert
|
||||||
class="w-[80%] md:w-[50%] lg:w-[30%] h-[60%]"
|
v-if="priceLocked"
|
||||||
>
|
type="warning"
|
||||||
|
show-icon
|
||||||
|
class="mb-3"
|
||||||
|
message="该商品不可改价"
|
||||||
|
description="已绑定配送仓库,商家侧不允许修改售价。"
|
||||||
|
/>
|
||||||
|
<Alert
|
||||||
|
v-else-if="priceRangeTip"
|
||||||
|
type="info"
|
||||||
|
show-icon
|
||||||
|
class="mb-3"
|
||||||
|
:message="priceRangeTip"
|
||||||
|
description="售价必须落在平台设置的底价~最高价区间内。"
|
||||||
|
/>
|
||||||
<Form />
|
<Form />
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -65,6 +65,40 @@ export const modalFormProps: VbenFormProps = {
|
|||||||
formItemClass: 'col-span-12',
|
formItemClass: 'col-span-12',
|
||||||
label: '价格',
|
label: '价格',
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
|
// 绑定配送仓时禁用改价;区间提示由弹窗文案补充
|
||||||
|
dependencies: {
|
||||||
|
disabled(values: any) {
|
||||||
|
return Number(values?.has_delivery_warehouse) === 1;
|
||||||
|
},
|
||||||
|
triggerFields: ['has_delivery_warehouse'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
fieldName: 'has_delivery_warehouse',
|
||||||
|
label: '配送仓',
|
||||||
|
dependencies: {
|
||||||
|
show: false,
|
||||||
|
triggerFields: ['has_delivery_warehouse'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
fieldName: 'min_adjust_price',
|
||||||
|
label: '底价',
|
||||||
|
dependencies: {
|
||||||
|
show: false,
|
||||||
|
triggerFields: ['min_adjust_price'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
fieldName: 'max_adjust_price',
|
||||||
|
label: '最高价',
|
||||||
|
dependencies: {
|
||||||
|
show: false,
|
||||||
|
triggerFields: ['max_adjust_price'],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
showDefaultActions: false,
|
showDefaultActions: false,
|
||||||
|
|||||||
@@ -62,9 +62,12 @@ const [ExcelUploadModal, ExcelUploadModalApi] = useVbenModal({
|
|||||||
connectedComponent: ExcelUpload,
|
connectedComponent: ExcelUpload,
|
||||||
});
|
});
|
||||||
|
|
||||||
const showModal = (data = {}, isUpdate = false) => {
|
const showModal = (data: any = {}, isUpdate = false) => {
|
||||||
|
if (isUpdate && Number(data?.has_delivery_warehouse) === 1) {
|
||||||
|
message.warning('该商品不可改价');
|
||||||
|
return;
|
||||||
|
}
|
||||||
formModalApi.setData({
|
formModalApi.setData({
|
||||||
// 表单值
|
|
||||||
values: data,
|
values: data,
|
||||||
update: isUpdate,
|
update: isUpdate,
|
||||||
gridApi,
|
gridApi,
|
||||||
@@ -254,31 +257,15 @@ checkSubscribe();
|
|||||||
<template #action="{ row }">
|
<template #action="{ row }">
|
||||||
<TableAction
|
<TableAction
|
||||||
:actions="[
|
:actions="[
|
||||||
// {
|
|
||||||
// label: '上架/下架(仓库)',
|
|
||||||
// type: 'link',
|
|
||||||
// icon: 'uil:edit',
|
|
||||||
// size: 'small',
|
|
||||||
// // auth: ['western-medicine', 'sys:role:detail'],
|
|
||||||
// onClick: updateStatus.bind(null, row.id),
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// label: '上架/下架(商城)',
|
|
||||||
// type: 'link',
|
|
||||||
// icon: 'uil:edit',
|
|
||||||
// size: 'small',
|
|
||||||
// // auth: ['western-medicine', 'sys:role:detail'],
|
|
||||||
// onClick: updateWarehouseDrugManagementShopUpdateStatus.bind(
|
|
||||||
// null,
|
|
||||||
// row.id,
|
|
||||||
// ),
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
label: '编辑',
|
label:
|
||||||
|
Number(row.has_delivery_warehouse) === 1
|
||||||
|
? '不可改价'
|
||||||
|
: '编辑',
|
||||||
type: 'link',
|
type: 'link',
|
||||||
icon: 'uil:edit',
|
icon: 'uil:edit',
|
||||||
size: 'small',
|
size: 'small',
|
||||||
// auth: ['western-medicine', 'sys:role:detail'],
|
disabled: Number(row.has_delivery_warehouse) === 1,
|
||||||
onClick: showModal.bind(null, row, true),
|
onClick: showModal.bind(null, row, true),
|
||||||
},
|
},
|
||||||
]"
|
]"
|
||||||
|
|||||||
@@ -50,11 +50,21 @@ export async function updateWarehouseDrugManagementShopUpdateStatusApi(id: numbe
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 导出仓库药品药品
|
* 导出仓库药品(旧:后端二进制,保留兼容)
|
||||||
*/
|
*/
|
||||||
export async function exportWarehouseDrugManagementStoreApi(params?: { type?: number }) {
|
export async function exportWarehouseDrugManagementStoreApi(params?: { type?: number }) {
|
||||||
return requestClient.download(`${prefix}export`, { params });
|
return requestClient.download(`${prefix}export`, { params });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出 JSON 数据(供前端 ExcelJS)
|
||||||
|
*/
|
||||||
|
export async function getStoreExportDataApi(params?: { type?: number }) {
|
||||||
|
return requestClient.get<{
|
||||||
|
total: number;
|
||||||
|
rows: Array<Record<string, unknown>>;
|
||||||
|
}>(`${prefix}export-data`, { params });
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* 导出仓库药品药品 - 模板
|
* 导出仓库药品药品 - 模板
|
||||||
*/
|
*/
|
||||||
@@ -95,13 +105,65 @@ export async function importWarehouseDrugManagementStoreApi(data: Record<string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 导入仓库药品修改价格
|
* 导入仓库药品修改价格(兼容旧直传入口)
|
||||||
* @param data
|
* @param data
|
||||||
*/
|
*/
|
||||||
export async function importUpdatePriceApi(data: Record<string, any>) {
|
export async function importUpdatePriceApi(data: Record<string, any>) {
|
||||||
return requestClient.upload(`${prefix}import-update-price`, data);
|
return requestClient.upload(`${prefix}import-update-price`, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface StoreBatchPriceItem {
|
||||||
|
id: number;
|
||||||
|
drug_id: number;
|
||||||
|
drug_name: string;
|
||||||
|
drug_number: string;
|
||||||
|
pinyin_simple: string;
|
||||||
|
unit_name?: string;
|
||||||
|
price: number | string;
|
||||||
|
buy_price?: number | string;
|
||||||
|
has_delivery_warehouse?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StoreCentralAvailableItem {
|
||||||
|
drug_id: number;
|
||||||
|
drug_name: string;
|
||||||
|
drug_number: string;
|
||||||
|
pinyin_simple: string;
|
||||||
|
unit_name?: string;
|
||||||
|
price: number | string;
|
||||||
|
buy_price?: number | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 门店批量改价预览:全量快照 */
|
||||||
|
export async function getStoreAllForBatchPriceApi(params: { type: number }) {
|
||||||
|
return requestClient.get<{
|
||||||
|
items: StoreBatchPriceItem[];
|
||||||
|
central_available?: StoreCentralAvailableItem[];
|
||||||
|
}>(`${prefix}list-all-for-batch-price`, { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StoreBatchPriceUpdateItem {
|
||||||
|
id: number;
|
||||||
|
price: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StoreBatchPriceCreateItem {
|
||||||
|
drug_id: number;
|
||||||
|
price: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 门店批量改价预览:确认落库(仅零售价) */
|
||||||
|
export async function applyStoreBatchPriceApi(data: {
|
||||||
|
type: number;
|
||||||
|
updates?: StoreBatchPriceUpdateItem[];
|
||||||
|
creates?: StoreBatchPriceCreateItem[];
|
||||||
|
}) {
|
||||||
|
return requestClient.post<{
|
||||||
|
update_count: number;
|
||||||
|
new_count: number;
|
||||||
|
}>(`${prefix}apply-batch-price`, data);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 同步总仓库api
|
* 同步总仓库api
|
||||||
* @param data
|
* @param data
|
||||||
|
|||||||
@@ -0,0 +1,687 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
/**
|
||||||
|
* 门店仓库批量改价预览:ExcelJS 本地解析 → 分类对比 → 确认落库
|
||||||
|
* 仅改零售价;Excel 含单位列时忽略并提示;配送仓锁定行不可提交
|
||||||
|
*/
|
||||||
|
import type { UploadFile } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Button, message, Spin, Tag, UploadDragger } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { downloadByData } from '#/util/tool';
|
||||||
|
|
||||||
|
import {
|
||||||
|
applyStoreBatchPriceApi,
|
||||||
|
getStoreAllForBatchPriceApi,
|
||||||
|
getStoreExportDataApi,
|
||||||
|
type StoreBatchPriceItem,
|
||||||
|
type StoreCentralAvailableItem,
|
||||||
|
} from '../api';
|
||||||
|
import {
|
||||||
|
exportStoreListExcel,
|
||||||
|
type StoreExportRow,
|
||||||
|
} from '../utils/exportStorePriceExcel';
|
||||||
|
import { parseStorePriceExcelBuffer } from '../utils/parseStorePriceExcel';
|
||||||
|
import {
|
||||||
|
getPriceDelta,
|
||||||
|
isFieldChanged,
|
||||||
|
parsePriceValue,
|
||||||
|
reconcileExcelPrice,
|
||||||
|
roundPrice4,
|
||||||
|
} from '../../admin/utils/priceCompare';
|
||||||
|
import PriceCompareCardGrid, {
|
||||||
|
type PriceCompareFilterTab,
|
||||||
|
type PriceCompareRow,
|
||||||
|
type UnmatchedRow,
|
||||||
|
} from '../../shared/components/PriceCompareCardGrid.vue';
|
||||||
|
|
||||||
|
const gridApi = ref();
|
||||||
|
const productType = ref(1);
|
||||||
|
const fileList = ref<UploadFile[]>([]);
|
||||||
|
const selectedFile = ref<File | null>(null);
|
||||||
|
const loadingAll = ref(false);
|
||||||
|
const parseLoading = ref(false);
|
||||||
|
const applyLoading = ref(false);
|
||||||
|
const parseHint = ref('');
|
||||||
|
const parsedReady = ref(false);
|
||||||
|
const filterTab = ref<PriceCompareFilterTab>('all');
|
||||||
|
|
||||||
|
const storeItems = ref<StoreBatchPriceItem[]>([]);
|
||||||
|
const centralAvailable = ref<StoreCentralAvailableItem[]>([]);
|
||||||
|
/** relation.id → 原始零售价 */
|
||||||
|
const initialPriceMap = ref<Record<number, number>>({});
|
||||||
|
/** relation.id → 当前编辑零售价 */
|
||||||
|
const localPriceMap = ref<Record<number, number>>({});
|
||||||
|
const newRows = ref<
|
||||||
|
Array<{
|
||||||
|
tempId: number;
|
||||||
|
drug_id: number;
|
||||||
|
drug_name: string;
|
||||||
|
drug_number: string;
|
||||||
|
unit_name: string;
|
||||||
|
price: number;
|
||||||
|
}>
|
||||||
|
>([]);
|
||||||
|
const unmatchedRows = ref<UnmatchedRow[]>([]);
|
||||||
|
const excelTouchedIds = ref<Set<number>>(new Set());
|
||||||
|
let tempIdCounter = -1;
|
||||||
|
let loadAllPromise: Promise<void> | null = null;
|
||||||
|
|
||||||
|
function resetState() {
|
||||||
|
selectedFile.value = null;
|
||||||
|
fileList.value = [];
|
||||||
|
storeItems.value = [];
|
||||||
|
centralAvailable.value = [];
|
||||||
|
initialPriceMap.value = {};
|
||||||
|
localPriceMap.value = {};
|
||||||
|
newRows.value = [];
|
||||||
|
unmatchedRows.value = [];
|
||||||
|
excelTouchedIds.value = new Set();
|
||||||
|
parseHint.value = '';
|
||||||
|
parsedReady.value = false;
|
||||||
|
filterTab.value = 'all';
|
||||||
|
tempIdCounter = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearParseResult() {
|
||||||
|
newRows.value = [];
|
||||||
|
unmatchedRows.value = [];
|
||||||
|
excelTouchedIds.value = new Set();
|
||||||
|
parseHint.value = '';
|
||||||
|
parsedReady.value = false;
|
||||||
|
const next: Record<number, number> = {};
|
||||||
|
for (const [id, price] of Object.entries(initialPriceMap.value)) {
|
||||||
|
next[Number(id)] = price;
|
||||||
|
}
|
||||||
|
localPriceMap.value = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSnapshot(items: StoreBatchPriceItem[]) {
|
||||||
|
const init: Record<number, number> = {};
|
||||||
|
const loc: Record<number, number> = {};
|
||||||
|
for (const item of items) {
|
||||||
|
// 与 Excel 解析同一套 4 位小数,避免 format_price 把浮点误差打成「降一分」
|
||||||
|
const price = roundPrice4(item.price);
|
||||||
|
init[item.id] = price;
|
||||||
|
loc[item.id] = price;
|
||||||
|
}
|
||||||
|
initialPriceMap.value = init;
|
||||||
|
localPriceMap.value = loc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组装对比行:门店仅零售价;配送仓锁定标记 _priceLocked
|
||||||
|
*/
|
||||||
|
function buildCompareRow(
|
||||||
|
item: {
|
||||||
|
id: number;
|
||||||
|
drug_id?: number;
|
||||||
|
drug_name: string;
|
||||||
|
drug_number: string;
|
||||||
|
pinyin_simple?: string;
|
||||||
|
unit_name?: string;
|
||||||
|
has_delivery_warehouse?: number;
|
||||||
|
},
|
||||||
|
currentPrice: number,
|
||||||
|
origPrice: number,
|
||||||
|
isNew: boolean,
|
||||||
|
): PriceCompareRow {
|
||||||
|
const priceLocked = !isNew && Number(item.has_delivery_warehouse) === 1;
|
||||||
|
const priceChanged = isFieldChanged(origPrice, currentPrice);
|
||||||
|
// 锁定行即使 Excel 改了价也不算「有修改」,避免误提交
|
||||||
|
const changed = isNew || (!priceLocked && priceChanged);
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
drug_id: item.drug_id,
|
||||||
|
drug_name: item.drug_name,
|
||||||
|
drug_number: item.drug_number,
|
||||||
|
pinyin_simple: item.pinyin_simple ?? '',
|
||||||
|
orig_market_price: 0,
|
||||||
|
orig_price: origPrice,
|
||||||
|
orig_unit_name: item.unit_name ?? '',
|
||||||
|
market_price: 0,
|
||||||
|
price: currentPrice,
|
||||||
|
unit_name: item.unit_name ?? '',
|
||||||
|
_changed: changed,
|
||||||
|
_marketGtPrice: false,
|
||||||
|
_unitChanged: false,
|
||||||
|
_isNew: isNew,
|
||||||
|
_fromExcel: excelTouchedIds.value.has(item.id) || isNew,
|
||||||
|
_priceLocked: priceLocked,
|
||||||
|
_marketDelta: 'same',
|
||||||
|
_priceDelta: getPriceDelta(origPrice, currentPrice),
|
||||||
|
_marketChanged: false,
|
||||||
|
_priceChanged: priceChanged,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const compareRows = computed<PriceCompareRow[]>(() => {
|
||||||
|
const existing = storeItems.value.map((item) => {
|
||||||
|
const orig = initialPriceMap.value[item.id] ?? parsePriceValue(item.price);
|
||||||
|
const current = localPriceMap.value[item.id] ?? orig;
|
||||||
|
return buildCompareRow(item, current, orig, false);
|
||||||
|
});
|
||||||
|
const news = newRows.value.map((row) =>
|
||||||
|
buildCompareRow(
|
||||||
|
{
|
||||||
|
id: row.tempId,
|
||||||
|
drug_id: row.drug_id,
|
||||||
|
drug_name: row.drug_name,
|
||||||
|
drug_number: row.drug_number,
|
||||||
|
unit_name: row.unit_name,
|
||||||
|
},
|
||||||
|
row.price,
|
||||||
|
0,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return [...existing, ...news];
|
||||||
|
});
|
||||||
|
|
||||||
|
const changedCount = computed(
|
||||||
|
() =>
|
||||||
|
compareRows.value.filter((r) => r._changed && !r._isNew && !r._priceLocked)
|
||||||
|
.length,
|
||||||
|
);
|
||||||
|
const newCount = computed(
|
||||||
|
() => compareRows.value.filter((r) => r._isNew).length,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [ModalComp, modalApi] = useVbenModal({
|
||||||
|
fullscreen: true,
|
||||||
|
fullscreenButton: true,
|
||||||
|
draggable: true,
|
||||||
|
confirmText: '批量确认更新',
|
||||||
|
cancelText: '关闭',
|
||||||
|
showConfirmButton: true,
|
||||||
|
onConfirm: runBatchApply,
|
||||||
|
onCancel() {
|
||||||
|
resetState();
|
||||||
|
modalApi.close();
|
||||||
|
},
|
||||||
|
onOpenChange(isOpen: boolean) {
|
||||||
|
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
|
||||||
|
productType.value = isOpen ? modalApi.getData()?.productType ?? 1 : 1;
|
||||||
|
if (isOpen) {
|
||||||
|
modalApi.setState({ fullscreen: true });
|
||||||
|
syncModalFooterState();
|
||||||
|
loadAllDrugs();
|
||||||
|
} else {
|
||||||
|
resetState();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function syncModalFooterState() {
|
||||||
|
modalApi.setState({
|
||||||
|
confirmDisabled:
|
||||||
|
!parsedReady.value ||
|
||||||
|
(changedCount.value === 0 && newCount.value === 0),
|
||||||
|
confirmLoading: applyLoading.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[changedCount, newCount, applyLoading, parseLoading, parsedReady],
|
||||||
|
syncModalFooterState,
|
||||||
|
);
|
||||||
|
|
||||||
|
async function loadAllDrugs() {
|
||||||
|
loadingAll.value = true;
|
||||||
|
const task = (async () => {
|
||||||
|
try {
|
||||||
|
const res = await getStoreAllForBatchPriceApi({ type: productType.value });
|
||||||
|
storeItems.value = res?.items ?? [];
|
||||||
|
centralAvailable.value = res?.central_available ?? [];
|
||||||
|
buildSnapshot(storeItems.value);
|
||||||
|
} catch {
|
||||||
|
message.error('加载门店药品失败');
|
||||||
|
storeItems.value = [];
|
||||||
|
centralAvailable.value = [];
|
||||||
|
initialPriceMap.value = {};
|
||||||
|
localPriceMap.value = {};
|
||||||
|
} finally {
|
||||||
|
loadingAll.value = false;
|
||||||
|
loadAllPromise = null;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
loadAllPromise = task;
|
||||||
|
await task;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleFileChange(info: { file: UploadFile }) {
|
||||||
|
const { file } = info;
|
||||||
|
if (file.status === 'removed') {
|
||||||
|
selectedFile.value = null;
|
||||||
|
clearParseResult();
|
||||||
|
filterTab.value = 'all';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const raw = file.originFileObj ?? file;
|
||||||
|
if (raw instanceof File) {
|
||||||
|
selectedFile.value = raw;
|
||||||
|
await runParse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载标准模板:与列表页「导出」同一套 export-data + 列结构
|
||||||
|
*/
|
||||||
|
async function downloadTemplate() {
|
||||||
|
try {
|
||||||
|
const res = await getStoreExportDataApi({ type: productType.value });
|
||||||
|
const rows = (res?.rows ?? []) as StoreExportRow[];
|
||||||
|
if (rows.length === 0) {
|
||||||
|
message.warning('暂无药品数据');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const buffer = await exportStoreListExcel(rows, '门店仓库');
|
||||||
|
downloadByData(
|
||||||
|
buffer,
|
||||||
|
'萧康云医-门店仓库商品导出.xlsx',
|
||||||
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
);
|
||||||
|
message.success(`模板已下载(${res?.total ?? rows.length} 条,与导出一致)`);
|
||||||
|
} catch {
|
||||||
|
message.error('模板下载失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findStoreByDrugId(drugId: number) {
|
||||||
|
return storeItems.value.find((d) => d.drug_id === drugId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findCentralByDrugId(drugId: number) {
|
||||||
|
return centralAvailable.value.find((d) => d.drug_id === drugId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runParse() {
|
||||||
|
if (!selectedFile.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (loadingAll.value && loadAllPromise) {
|
||||||
|
await loadAllPromise;
|
||||||
|
}
|
||||||
|
parseLoading.value = true;
|
||||||
|
syncModalFooterState();
|
||||||
|
try {
|
||||||
|
const buf = await selectedFile.value.arrayBuffer();
|
||||||
|
const { items, invalidCount, rowCount, hasUnitColumn } =
|
||||||
|
await parseStorePriceExcelBuffer(buf);
|
||||||
|
|
||||||
|
if (hasUnitColumn) {
|
||||||
|
message.info('门店导入已忽略单位列(单位仅平台总仓可改)');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (items.length === 0) {
|
||||||
|
message.warning(
|
||||||
|
invalidCount > 0
|
||||||
|
? `未解析到有效行(${invalidCount} 行缺少合法药品ID)`
|
||||||
|
: '未解析到有效的价格行',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hints: string[] = [
|
||||||
|
rowCount > 5000
|
||||||
|
? `共 ${rowCount} 行,已按上限处理前 5000 行。`
|
||||||
|
: `共解析 ${items.length} 条有效行。`,
|
||||||
|
];
|
||||||
|
if (hasUnitColumn) {
|
||||||
|
hints.push('已忽略单位列。');
|
||||||
|
}
|
||||||
|
if (invalidCount > 0) {
|
||||||
|
hints.push(`${invalidCount} 行因药品ID无效已跳过。`);
|
||||||
|
}
|
||||||
|
parseHint.value = hints.join(' ');
|
||||||
|
|
||||||
|
const nextUnmatched: UnmatchedRow[] = [];
|
||||||
|
const nextNew: typeof newRows.value = [];
|
||||||
|
const nextLocal = { ...localPriceMap.value };
|
||||||
|
const nextTouched = new Set<number>();
|
||||||
|
|
||||||
|
for (const row of items) {
|
||||||
|
const matched = findStoreByDrugId(row.drug_id);
|
||||||
|
if (matched) {
|
||||||
|
nextTouched.add(matched.id);
|
||||||
|
const orig = initialPriceMap.value[matched.id] ?? parsePriceValue(matched.price);
|
||||||
|
// 与快照对账,消化 Excel 一分钱浮点误差
|
||||||
|
nextLocal[matched.id] = reconcileExcelPrice(row.price, orig);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const central = findCentralByDrugId(row.drug_id);
|
||||||
|
if (central) {
|
||||||
|
nextNew.push({
|
||||||
|
tempId: tempIdCounter--,
|
||||||
|
drug_id: central.drug_id,
|
||||||
|
drug_name: central.drug_name || row.drug_name,
|
||||||
|
drug_number: central.drug_number || row.drug_number,
|
||||||
|
unit_name: central.unit_name ?? '',
|
||||||
|
price: row.price,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
nextUnmatched.push({
|
||||||
|
drug_name: row.drug_name || `ID:${row.drug_id}`,
|
||||||
|
drug_number: row.drug_number || String(row.drug_id),
|
||||||
|
market_price: 0,
|
||||||
|
price: row.price,
|
||||||
|
unit_name: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
localPriceMap.value = nextLocal;
|
||||||
|
excelTouchedIds.value = nextTouched;
|
||||||
|
newRows.value = nextNew;
|
||||||
|
unmatchedRows.value = nextUnmatched;
|
||||||
|
parsedReady.value = true;
|
||||||
|
|
||||||
|
const changedN = storeItems.value.filter((item) => {
|
||||||
|
if (!nextTouched.has(item.id)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const orig = initialPriceMap.value[item.id];
|
||||||
|
const cur = nextLocal[item.id];
|
||||||
|
return (
|
||||||
|
Number(item.has_delivery_warehouse) !== 1 &&
|
||||||
|
isFieldChanged(orig, cur)
|
||||||
|
);
|
||||||
|
}).length;
|
||||||
|
|
||||||
|
if (changedN > 0) {
|
||||||
|
filterTab.value = 'changed';
|
||||||
|
} else if (nextNew.length > 0) {
|
||||||
|
filterTab.value = 'new';
|
||||||
|
} else {
|
||||||
|
filterTab.value = 'unchanged';
|
||||||
|
message.info('未检测到零售价变更');
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
message.error('解析失败,请检查 Excel 格式');
|
||||||
|
} finally {
|
||||||
|
parseLoading.value = false;
|
||||||
|
syncModalFooterState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncRowFromGrid(row: PriceCompareRow) {
|
||||||
|
if (row._isNew) {
|
||||||
|
newRows.value = newRows.value.map((item) =>
|
||||||
|
item.tempId === row.id ? { ...item, price: row.price } : item,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
localPriceMap.value = {
|
||||||
|
...localPriceMap.value,
|
||||||
|
[row.id]: row.price,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runBatchApply() {
|
||||||
|
const changedExisting = compareRows.value.filter(
|
||||||
|
(r) => r._changed && !r._isNew && !r._priceLocked,
|
||||||
|
);
|
||||||
|
const creates = newRows.value.map((row) => ({
|
||||||
|
drug_id: row.drug_id,
|
||||||
|
price: row.price,
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (changedExisting.length === 0 && creates.length === 0) {
|
||||||
|
message.info('没有需要保存的变更');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
applyLoading.value = true;
|
||||||
|
modalApi.setState({ loading: true, confirmLoading: true });
|
||||||
|
try {
|
||||||
|
const res = await applyStoreBatchPriceApi({
|
||||||
|
type: productType.value,
|
||||||
|
updates: changedExisting.map((row) => ({
|
||||||
|
id: row.id,
|
||||||
|
price: parsePriceValue(row.price),
|
||||||
|
})),
|
||||||
|
creates,
|
||||||
|
});
|
||||||
|
const updateN = res?.update_count ?? 0;
|
||||||
|
const newN = res?.new_count ?? 0;
|
||||||
|
message.success(`已更新 ${updateN} 条,新增 ${newN} 条`);
|
||||||
|
gridApi.value?.reload?.();
|
||||||
|
resetState();
|
||||||
|
modalApi.close();
|
||||||
|
} catch {
|
||||||
|
message.error('批量更新失败');
|
||||||
|
} finally {
|
||||||
|
applyLoading.value = false;
|
||||||
|
modalApi.setState({ loading: false, confirmLoading: false });
|
||||||
|
syncModalFooterState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ModalComp title="批量改价预览(门店仓库)">
|
||||||
|
<Transition name="fade-slide" mode="out-in">
|
||||||
|
<div
|
||||||
|
v-if="!parsedReady"
|
||||||
|
class="flex h-[calc(100vh-220px)] w-full flex-col items-center justify-center px-4 sm:px-12 md:px-24"
|
||||||
|
>
|
||||||
|
<div class="relative w-full max-w-[85vw] xl:max-w-screen-xl">
|
||||||
|
<div
|
||||||
|
v-if="loadingAll || parseLoading"
|
||||||
|
class="absolute inset-0 z-50 flex items-center justify-center rounded-3xl bg-white/70 backdrop-blur-[2px] dark:bg-gray-900/70"
|
||||||
|
>
|
||||||
|
<Spin size="large" tip="解析数据中..." />
|
||||||
|
</div>
|
||||||
|
<div class="flex w-full flex-col items-center gap-8">
|
||||||
|
<div class="text-center">
|
||||||
|
<h2 class="mb-3 text-3xl font-medium text-gray-800 dark:text-gray-100">
|
||||||
|
导入门店零售价
|
||||||
|
</h2>
|
||||||
|
<p class="text-lg text-gray-500">
|
||||||
|
上传 Excel 预览有变动 / 无变动 / 待新增后再确认;单位列会被忽略
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="batch-price-upload batch-price-upload--hero w-full transition-transform duration-300 hover:scale-[1.01]">
|
||||||
|
<UploadDragger
|
||||||
|
v-model:file-list="fileList"
|
||||||
|
:before-upload="() => false"
|
||||||
|
:max-count="1"
|
||||||
|
:show-upload-list="false"
|
||||||
|
accept=".xlsx,.xls"
|
||||||
|
name="file"
|
||||||
|
@change="handleFileChange"
|
||||||
|
>
|
||||||
|
<div class="flex min-h-[420px] w-full flex-col items-center justify-center py-16">
|
||||||
|
<div class="mb-8 rounded-full bg-blue-50 p-6 text-blue-500 ring-8 ring-blue-50/50 dark:bg-blue-900/30 dark:ring-blue-900/20">
|
||||||
|
<svg class="h-14 w-14" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p class="text-2xl font-medium text-gray-700 dark:text-gray-200">
|
||||||
|
点击或将 Excel 文件拖拽至此
|
||||||
|
</p>
|
||||||
|
<p class="mt-4 text-lg text-gray-400">
|
||||||
|
纯前端本地解析,确认后才提交服务器
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</UploadDragger>
|
||||||
|
</div>
|
||||||
|
<div class="flex min-h-[28px] items-center gap-2">
|
||||||
|
<p
|
||||||
|
v-if="selectedFile"
|
||||||
|
class="text-lg font-medium text-blue-600 dark:text-blue-400"
|
||||||
|
>
|
||||||
|
已选文件:{{ selectedFile.name }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
:disabled="loadingAll || storeItems.length === 0"
|
||||||
|
size="large"
|
||||||
|
type="dashed"
|
||||||
|
class="!rounded-full px-10 py-2 shadow-sm"
|
||||||
|
@click="downloadTemplate"
|
||||||
|
>
|
||||||
|
下载标准模板
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="flex w-full flex-col gap-4"
|
||||||
|
style="height: calc(100vh - 200px)"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex shrink-0 flex-col gap-4 rounded-xl border border-gray-100 bg-gray-50/50 p-4 shadow-sm backdrop-blur-sm dark:border-gray-800 dark:bg-gray-900/30 sm:flex-row sm:items-center sm:justify-between"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="batch-price-upload batch-price-upload--mini">
|
||||||
|
<UploadDragger
|
||||||
|
v-model:file-list="fileList"
|
||||||
|
:before-upload="() => false"
|
||||||
|
:max-count="1"
|
||||||
|
:show-upload-list="false"
|
||||||
|
accept=".xlsx,.xls"
|
||||||
|
name="file"
|
||||||
|
class="!rounded-lg"
|
||||||
|
@change="handleFileChange"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2 px-4 py-1.5 text-sm font-medium text-gray-600 dark:text-gray-300">
|
||||||
|
重新上传
|
||||||
|
</div>
|
||||||
|
</UploadDragger>
|
||||||
|
</div>
|
||||||
|
<div v-if="selectedFile" class="text-sm text-gray-500">
|
||||||
|
当前:
|
||||||
|
<span class="font-medium text-gray-700 dark:text-gray-200">
|
||||||
|
{{ selectedFile.name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<Button size="small" type="link" @click="downloadTemplate">
|
||||||
|
下载模板
|
||||||
|
</Button>
|
||||||
|
<div class="h-4 w-px bg-gray-300 dark:bg-gray-700"></div>
|
||||||
|
<Button
|
||||||
|
v-if="selectedFile"
|
||||||
|
:disabled="parseLoading"
|
||||||
|
size="small"
|
||||||
|
type="link"
|
||||||
|
@click="runParse"
|
||||||
|
>
|
||||||
|
重新解析
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="relative flex min-h-0 flex-1 flex-col overflow-hidden rounded-xl bg-white shadow-sm ring-1 ring-gray-100 dark:bg-gray-900 dark:ring-gray-800"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="loadingAll || parseLoading"
|
||||||
|
class="absolute inset-0 z-50 flex items-center justify-center bg-white/70 backdrop-blur-[2px] dark:bg-gray-900/70"
|
||||||
|
>
|
||||||
|
<Spin size="large" tip="数据处理中..." />
|
||||||
|
</div>
|
||||||
|
<div class="custom-scrollbar relative w-full flex-1 overflow-y-auto p-4">
|
||||||
|
<PriceCompareCardGrid
|
||||||
|
v-model:filter-tab="filterTab"
|
||||||
|
:rows="compareRows"
|
||||||
|
:unmatched-rows="unmatchedRows"
|
||||||
|
:show-unit="false"
|
||||||
|
:show-market-price="false"
|
||||||
|
:show-row-save="false"
|
||||||
|
:show-price-locked-tab="true"
|
||||||
|
price-label="零售价"
|
||||||
|
@cell-change="syncRowFromGrid"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
|
||||||
|
<template v-if="parsedReady" #prepend-footer>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Tag
|
||||||
|
:color="changedCount > 0 ? 'warning' : 'default'"
|
||||||
|
class="!rounded-md shadow-sm"
|
||||||
|
>
|
||||||
|
待保存修改 {{ changedCount }}
|
||||||
|
</Tag>
|
||||||
|
<Tag
|
||||||
|
:color="newCount > 0 ? 'processing' : 'default'"
|
||||||
|
class="!rounded-md shadow-sm"
|
||||||
|
>
|
||||||
|
待新增数据 {{ newCount }}
|
||||||
|
</Tag>
|
||||||
|
<span v-if="parseHint" class="ml-2 text-xs text-gray-400">
|
||||||
|
{{ parseHint }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ModalComp>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.fade-slide-enter-active,
|
||||||
|
.fade-slide-leave-active {
|
||||||
|
transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||||
|
}
|
||||||
|
.fade-slide-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
.fade-slide-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-20px);
|
||||||
|
}
|
||||||
|
.custom-scrollbar::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||||
|
background-color: rgba(156, 163, 175, 0.5);
|
||||||
|
border-radius: 9999px;
|
||||||
|
}
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: rgba(107, 114, 128, 0.8);
|
||||||
|
}
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.batch-price-upload {
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.batch-price-upload :deep(.ant-upload-wrapper),
|
||||||
|
.batch-price-upload :deep(.ant-upload-list),
|
||||||
|
.batch-price-upload :deep(.ant-upload-drag) {
|
||||||
|
width: 100% !important;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.batch-price-upload--hero :deep(.ant-upload-drag) {
|
||||||
|
background: rgba(249, 250, 251, 0.5);
|
||||||
|
border: 2px dashed #d1d5db;
|
||||||
|
border-radius: 1.5rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
.batch-price-upload--hero :deep(.ant-upload-drag:hover) {
|
||||||
|
border-color: #3b82f6;
|
||||||
|
background: rgba(239, 246, 255, 0.5);
|
||||||
|
}
|
||||||
|
.batch-price-upload--mini :deep(.ant-upload-drag) {
|
||||||
|
border: 1px dashed #d1d5db;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background: transparent;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.batch-price-upload--mini :deep(.ant-upload-drag:hover) {
|
||||||
|
border-color: #3b82f6;
|
||||||
|
background: rgba(239, 246, 255, 0.5);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
/**
|
||||||
|
* 门店仓库药品编辑弹窗
|
||||||
|
* 绑定配送仓不可改价;有底价/最高价时提示改价区间
|
||||||
|
*/
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
import { useVbenModal } from '@vben/common-ui';
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
import { useUserStore } from '@vben/stores';
|
|
||||||
|
|
||||||
import { message } from 'ant-design-vue';
|
import { Alert, message } from 'ant-design-vue';
|
||||||
|
|
||||||
import { useVbenForm } from '#/adapter/form';
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
|
||||||
@@ -13,10 +16,7 @@ import {
|
|||||||
updateWarehouseDrugManagementStore,
|
updateWarehouseDrugManagementStore,
|
||||||
} from '../api';
|
} from '../api';
|
||||||
import { modalFormProps } from '../config/form';
|
import { modalFormProps } from '../config/form';
|
||||||
import {getDrugUseList} from "#/views/doctor/doctor-reception/api";
|
import { getDrugUseList } from '#/views/doctor/doctor-reception/api';
|
||||||
|
|
||||||
|
|
||||||
const userStore = useUserStore();
|
|
||||||
|
|
||||||
const drugTime = ref([]);
|
const drugTime = ref([]);
|
||||||
const drugType = ref([]);
|
const drugType = ref([]);
|
||||||
@@ -52,10 +52,13 @@ getDrugUseList().then((res) => {
|
|||||||
|
|
||||||
const isUpdate = ref(false);
|
const isUpdate = ref(false);
|
||||||
const gridApi = ref();
|
const gridApi = ref();
|
||||||
|
/** 是否绑定配送仓(不可改价) */
|
||||||
|
const priceLocked = ref(false);
|
||||||
|
/** 改价区间展示文案 */
|
||||||
|
const priceRangeTip = ref('');
|
||||||
|
|
||||||
const [Form, formApi] = useVbenForm(modalFormProps);
|
const [Form, formApi] = useVbenForm(modalFormProps);
|
||||||
|
|
||||||
|
|
||||||
const [Modal, modalApi] = useVbenModal({
|
const [Modal, modalApi] = useVbenModal({
|
||||||
fullscreenButton: false,
|
fullscreenButton: false,
|
||||||
draggable: true,
|
draggable: true,
|
||||||
@@ -63,6 +66,10 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
modalApi.close();
|
modalApi.close();
|
||||||
},
|
},
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
|
if (priceLocked.value) {
|
||||||
|
message.warning('该商品不可改价');
|
||||||
|
return;
|
||||||
|
}
|
||||||
formApi.validate().then(async (e: any) => {
|
formApi.validate().then(async (e: any) => {
|
||||||
if (e.valid) {
|
if (e.valid) {
|
||||||
const values = await formApi.getValues();
|
const values = await formApi.getValues();
|
||||||
@@ -86,7 +93,6 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
onOpenChange(isOpen: boolean) {
|
onOpenChange(isOpen: boolean) {
|
||||||
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
|
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
|
|
||||||
formApi.updateSchema([
|
formApi.updateSchema([
|
||||||
{
|
{
|
||||||
componentProps: {
|
componentProps: {
|
||||||
@@ -115,19 +121,61 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
]);
|
]);
|
||||||
const { values, update } = modalApi.getData<Record<string, any>>();
|
const { values, update } = modalApi.getData<Record<string, any>>();
|
||||||
if (values) {
|
if (values) {
|
||||||
console.log(values, 'sssssssssssss')
|
|
||||||
isUpdate.value = update;
|
isUpdate.value = update;
|
||||||
formApi.setValues(values);
|
// 从列表行带入配送仓标记与价格区间(drug 嵌套字段展平到表单)
|
||||||
|
const hasDelivery =
|
||||||
|
Number(values.has_delivery_warehouse) === 1 ||
|
||||||
|
Number(values.drug?.has_delivery_warehouse) === 1;
|
||||||
|
priceLocked.value = hasDelivery;
|
||||||
|
const min = Number(
|
||||||
|
values.min_adjust_price ?? values.drug?.min_adjust_price ?? 0,
|
||||||
|
);
|
||||||
|
const max = Number(
|
||||||
|
values.max_adjust_price ?? values.drug?.max_adjust_price ?? 0,
|
||||||
|
);
|
||||||
|
if (min > 0 || max > 0) {
|
||||||
|
const minTxt = min > 0 ? `¥${min.toFixed(2)}` : '不限';
|
||||||
|
const maxTxt = max > 0 ? `¥${max.toFixed(2)}` : '不限';
|
||||||
|
priceRangeTip.value = `改价区间:${minTxt} ~ ${maxTxt}`;
|
||||||
|
} else {
|
||||||
|
priceRangeTip.value = '';
|
||||||
|
}
|
||||||
|
formApi.setValues({
|
||||||
|
...values,
|
||||||
|
has_delivery_warehouse: hasDelivery ? 1 : 0,
|
||||||
|
min_adjust_price: min || null,
|
||||||
|
max_adjust_price: max || null,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
priceLocked.value = false;
|
||||||
|
priceRangeTip.value = '';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const modalTitle = computed(() =>
|
||||||
|
isUpdate.value === true ? '编辑仓库药品' : '新增仓库药品',
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<Modal
|
<Modal :title="modalTitle" class="w-[80%] md:w-[50%] lg:w-[30%] h-[60%]">
|
||||||
:title="`${isUpdate === true ? '编辑' : '新增'}仓库药品`"
|
<Alert
|
||||||
class="w-[80%] md:w-[50%] lg:w-[30%] h-[60%]"
|
v-if="priceLocked"
|
||||||
>
|
type="warning"
|
||||||
|
show-icon
|
||||||
|
class="mb-3"
|
||||||
|
message="该商品不可改价"
|
||||||
|
description="已绑定配送仓库,商家侧不允许修改售价。"
|
||||||
|
/>
|
||||||
|
<Alert
|
||||||
|
v-else-if="priceRangeTip"
|
||||||
|
type="info"
|
||||||
|
show-icon
|
||||||
|
class="mb-3"
|
||||||
|
:message="priceRangeTip"
|
||||||
|
description="售价必须落在平台设置的底价~最高价区间内。"
|
||||||
|
/>
|
||||||
<Form />
|
<Form />
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -65,6 +65,40 @@ export const modalFormProps: VbenFormProps = {
|
|||||||
formItemClass: 'col-span-12',
|
formItemClass: 'col-span-12',
|
||||||
label: '价格',
|
label: '价格',
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
|
// 绑定配送仓时禁用改价;区间提示由弹窗文案补充
|
||||||
|
dependencies: {
|
||||||
|
disabled(values: any) {
|
||||||
|
return Number(values?.has_delivery_warehouse) === 1;
|
||||||
|
},
|
||||||
|
triggerFields: ['has_delivery_warehouse'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
fieldName: 'has_delivery_warehouse',
|
||||||
|
label: '配送仓',
|
||||||
|
dependencies: {
|
||||||
|
show: false,
|
||||||
|
triggerFields: ['has_delivery_warehouse'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
fieldName: 'min_adjust_price',
|
||||||
|
label: '底价',
|
||||||
|
dependencies: {
|
||||||
|
show: false,
|
||||||
|
triggerFields: ['min_adjust_price'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
fieldName: 'max_adjust_price',
|
||||||
|
label: '最高价',
|
||||||
|
dependencies: {
|
||||||
|
show: false,
|
||||||
|
triggerFields: ['max_adjust_price'],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
showDefaultActions: false,
|
showDefaultActions: false,
|
||||||
|
|||||||
@@ -37,6 +37,12 @@ export function createGridOptions(productType: number): VxeGridProps<RowType> {
|
|||||||
{ field: 'buy_price', title: '供货价' },
|
{ field: 'buy_price', title: '供货价' },
|
||||||
{ field: 'drug.drug_store_drug.price', title: '建议售价' },
|
{ field: 'drug.drug_store_drug.price', title: '建议售价' },
|
||||||
{ field: 'price', title: '售价' },
|
{ field: 'price', title: '售价' },
|
||||||
|
{
|
||||||
|
field: 'has_delivery_warehouse',
|
||||||
|
title: '改价',
|
||||||
|
width: 100,
|
||||||
|
slots: { default: 'price_lock' },
|
||||||
|
},
|
||||||
{ field: 'status', title: '状态', slots: { default: 'status' } },
|
{ field: 'status', title: '状态', slots: { default: 'status' } },
|
||||||
{
|
{
|
||||||
type: 'html',
|
type: 'html',
|
||||||
|
|||||||
@@ -15,17 +15,21 @@ import { useWarehouseDrugTypeRoute } from '../shared/useWarehouseDrugTypeRoute';
|
|||||||
import {
|
import {
|
||||||
checkSubscribeApi,
|
checkSubscribeApi,
|
||||||
deleteWarehouseDrugManagementStore,
|
deleteWarehouseDrugManagementStore,
|
||||||
exportWarehouseDrugManagementStoreApi,
|
getStoreExportDataApi,
|
||||||
subscribeApi,
|
subscribeApi,
|
||||||
syncDrugApi,
|
syncDrugApi,
|
||||||
syncDrugPriceApi,
|
syncDrugPriceApi,
|
||||||
updateWarehouseDrugManagementShopUpdateStatusApi,
|
updateWarehouseDrugManagementShopUpdateStatusApi,
|
||||||
updateWarehouseDrugManagementStoreStatusApi,
|
updateWarehouseDrugManagementStoreStatusApi,
|
||||||
} from './api';
|
} from './api';
|
||||||
import ExcelUpload from './components/ExcelUpload.vue';
|
import StorePriceBatchModal from './components/StorePriceBatchModal.vue';
|
||||||
import FormModalDemo from './components/modal.vue';
|
import FormModalDemo from './components/modal.vue';
|
||||||
import { formOptions } from './config/search';
|
import { formOptions } from './config/search';
|
||||||
import { createGridOptions } from './config/table';
|
import { createGridOptions } from './config/table';
|
||||||
|
import {
|
||||||
|
exportStoreListExcel,
|
||||||
|
type StoreExportRow,
|
||||||
|
} from './utils/exportStorePriceExcel';
|
||||||
|
|
||||||
const { productType, typeLabel } = useWarehouseDrugTypeRoute(
|
const { productType, typeLabel } = useWarehouseDrugTypeRoute(
|
||||||
'/warehouse-drug-management-store/type/1',
|
'/warehouse-drug-management-store/type/1',
|
||||||
@@ -58,13 +62,17 @@ const [FormModal, formModalApi] = useVbenModal({
|
|||||||
connectedComponent: FormModalDemo,
|
connectedComponent: FormModalDemo,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [ExcelUploadModal, ExcelUploadModalApi] = useVbenModal({
|
const [StorePriceBatchModalComp, StorePriceBatchModalApi] = useVbenModal({
|
||||||
connectedComponent: ExcelUpload,
|
connectedComponent: StorePriceBatchModal,
|
||||||
});
|
});
|
||||||
|
|
||||||
const showModal = (data = {}, isUpdate = false) => {
|
const showModal = (data: any = {}, isUpdate = false) => {
|
||||||
|
// 绑定配送仓:直接提示不可改价,不打开编辑
|
||||||
|
if (isUpdate && Number(data?.has_delivery_warehouse) === 1) {
|
||||||
|
message.warning('该商品不可改价');
|
||||||
|
return;
|
||||||
|
}
|
||||||
formModalApi.setData({
|
formModalApi.setData({
|
||||||
// 表单值
|
|
||||||
values: data,
|
values: data,
|
||||||
update: isUpdate,
|
update: isUpdate,
|
||||||
gridApi,
|
gridApi,
|
||||||
@@ -84,24 +92,35 @@ const deleteApi = (row: any) => {
|
|||||||
gridApi.query();
|
gridApi.query();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
/**
|
|
||||||
* 导出
|
/** 打开门店批量改价预览(ExcelJS 解析后确认) */
|
||||||
*/
|
const openStorePriceBatchModal = () => {
|
||||||
const passApplication = () => {
|
StorePriceBatchModalApi.setData({
|
||||||
exportWarehouseDrugManagementStoreApi({ type: productType.value }).then((res) => {
|
gridApi,
|
||||||
downloadByData(res.data, '萧康云医-仓库商品导出.xlsx');
|
productType: productType.value,
|
||||||
message.success('导出成功!');
|
|
||||||
});
|
});
|
||||||
|
StorePriceBatchModalApi.open();
|
||||||
};
|
};
|
||||||
|
|
||||||
const openExcelUploadModal = (type = 1) => {
|
/** 门店导出:export-data + ExcelJS */
|
||||||
ExcelUploadModalApi.setData({
|
const exportStoreDrugs = async () => {
|
||||||
gridApi,
|
try {
|
||||||
type,
|
const res = await getStoreExportDataApi({ type: productType.value });
|
||||||
uploadType: productType.value,
|
const rows = (res?.rows ?? []) as StoreExportRow[];
|
||||||
passApplication,
|
if (rows.length === 0) {
|
||||||
});
|
message.warning('暂无药品数据');
|
||||||
ExcelUploadModalApi.open();
|
return;
|
||||||
|
}
|
||||||
|
const buffer = await exportStoreListExcel(rows, '门店仓库');
|
||||||
|
downloadByData(
|
||||||
|
buffer,
|
||||||
|
'萧康云医-门店仓库商品导出.xlsx',
|
||||||
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
);
|
||||||
|
message.success(`导出成功(${res?.total ?? rows.length} 条)`);
|
||||||
|
} catch {
|
||||||
|
message.error('导出失败');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const syncDrugs = () => {
|
const syncDrugs = () => {
|
||||||
@@ -153,17 +172,23 @@ checkSubscribe();
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Page auto-content-height :title="typeLabel || '门店仓库管理'">
|
<Page auto-content-height :title="typeLabel || '门店仓库管理'">
|
||||||
<ExcelUploadModal />
|
<StorePriceBatchModalComp />
|
||||||
<FormModal />
|
<FormModal />
|
||||||
<Grid v-if="productType">
|
<Grid v-if="productType">
|
||||||
<template #toolbar-buttons>
|
<template #toolbar-buttons>
|
||||||
<TableAction
|
<TableAction
|
||||||
:actions="[
|
:actions="[
|
||||||
{
|
{
|
||||||
label: '批量修改价格',
|
label: '批量改价预览',
|
||||||
type: 'primary',
|
type: 'primary',
|
||||||
icon: 'ix:export-check',
|
icon: 'mdi:file-compare',
|
||||||
onClick: openExcelUploadModal.bind(null, 2),
|
onClick: openStorePriceBatchModal,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '导出',
|
||||||
|
type: 'primary',
|
||||||
|
icon: 'lucide:download',
|
||||||
|
onClick: exportStoreDrugs,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '更新总仓库商品(只更新未同步的商品)',
|
label: '更新总仓库商品(只更新未同步的商品)',
|
||||||
@@ -237,6 +262,12 @@ checkSubscribe();
|
|||||||
@click="updateStatus(row.id)"
|
@click="updateStatus(row.id)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
<template #price_lock="{ row }">
|
||||||
|
<Tag v-if="Number(row.has_delivery_warehouse) === 1" color="orange">
|
||||||
|
不可改价
|
||||||
|
</Tag>
|
||||||
|
<span v-else class="text-slate-400">可改价</span>
|
||||||
|
</template>
|
||||||
<template #is_shop="{ row }">
|
<template #is_shop="{ row }">
|
||||||
<Switch
|
<Switch
|
||||||
:checked="row.is_shop"
|
:checked="row.is_shop"
|
||||||
@@ -251,31 +282,15 @@ checkSubscribe();
|
|||||||
<template #action="{ row }">
|
<template #action="{ row }">
|
||||||
<TableAction
|
<TableAction
|
||||||
:actions="[
|
:actions="[
|
||||||
// {
|
|
||||||
// label: '上架/下架(仓库)',
|
|
||||||
// type: 'link',
|
|
||||||
// icon: 'uil:edit',
|
|
||||||
// size: 'small',
|
|
||||||
// // auth: ['western-medicine', 'sys:role:detail'],
|
|
||||||
// onClick: updateStatus.bind(null, row.id),
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// label: '上架/下架(商城)',
|
|
||||||
// type: 'link',
|
|
||||||
// icon: 'uil:edit',
|
|
||||||
// size: 'small',
|
|
||||||
// // auth: ['western-medicine', 'sys:role:detail'],
|
|
||||||
// onClick: updateWarehouseDrugManagementShopUpdateStatus.bind(
|
|
||||||
// null,
|
|
||||||
// row.id,
|
|
||||||
// ),
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
label: '编辑',
|
label:
|
||||||
|
Number(row.has_delivery_warehouse) === 1
|
||||||
|
? '不可改价'
|
||||||
|
: '编辑',
|
||||||
type: 'link',
|
type: 'link',
|
||||||
icon: 'uil:edit',
|
icon: 'uil:edit',
|
||||||
size: 'small',
|
size: 'small',
|
||||||
// auth: ['western-medicine', 'sys:role:detail'],
|
disabled: Number(row.has_delivery_warehouse) === 1,
|
||||||
onClick: showModal.bind(null, row, true),
|
onClick: showModal.bind(null, row, true),
|
||||||
},
|
},
|
||||||
]"
|
]"
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
/**
|
||||||
|
* 门店仓库 ExcelJS 导出(数据来自 export-data)
|
||||||
|
* 价格/单位强制文本写出,避免 IEEE 浮点回读假变更
|
||||||
|
*/
|
||||||
|
import ExcelJS from 'exceljs';
|
||||||
|
|
||||||
|
const THIN_BORDER: Partial<ExcelJS.Borders> = {
|
||||||
|
top: { style: 'thin', color: { argb: 'FF000000' } },
|
||||||
|
left: { style: 'thin', color: { argb: 'FF000000' } },
|
||||||
|
bottom: { style: 'thin', color: { argb: 'FF000000' } },
|
||||||
|
right: { style: 'thin', color: { argb: 'FF000000' } },
|
||||||
|
};
|
||||||
|
|
||||||
|
export type StoreExportRow = {
|
||||||
|
drug_id?: number | string;
|
||||||
|
drug_number?: string;
|
||||||
|
drug_name?: string;
|
||||||
|
pinyin_simple?: string;
|
||||||
|
unit_name?: string;
|
||||||
|
type_text?: string;
|
||||||
|
status_text?: string;
|
||||||
|
buy_price?: string | number;
|
||||||
|
price?: string | number;
|
||||||
|
created_at?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const LIST_HEADERS = [
|
||||||
|
'药品ID',
|
||||||
|
'ErpCode',
|
||||||
|
'药名',
|
||||||
|
'拼音首拼',
|
||||||
|
'单位',
|
||||||
|
'类型',
|
||||||
|
'状态',
|
||||||
|
'供货价',
|
||||||
|
'当前零售价',
|
||||||
|
'创建时间',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const PRICE_HEADERS = [
|
||||||
|
'药品ID',
|
||||||
|
'ErpCode',
|
||||||
|
'药名',
|
||||||
|
'拼音首拼',
|
||||||
|
'单位',
|
||||||
|
'当前零售价',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
function priceToCellText(value: unknown): string {
|
||||||
|
if (value === null || value === undefined || value === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return String(value).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function forceTextCell(cell: ExcelJS.Cell, text: string) {
|
||||||
|
cell.value = text;
|
||||||
|
cell.numFmt = '@';
|
||||||
|
cell.border = THIN_BORDER;
|
||||||
|
cell.alignment = { horizontal: 'center', vertical: 'middle' };
|
||||||
|
}
|
||||||
|
|
||||||
|
function styleHeader(row: ExcelJS.Row) {
|
||||||
|
row.height = 28;
|
||||||
|
row.eachCell((cell) => {
|
||||||
|
cell.font = { bold: true };
|
||||||
|
cell.fill = {
|
||||||
|
type: 'pattern',
|
||||||
|
pattern: 'solid',
|
||||||
|
fgColor: { argb: 'FFFFFF00' },
|
||||||
|
};
|
||||||
|
cell.alignment = { horizontal: 'center', vertical: 'middle' };
|
||||||
|
cell.border = THIN_BORDER;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function styleDataRowFill(row: ExcelJS.Row, index: number) {
|
||||||
|
if (index % 2 !== 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
row.eachCell((cell) => {
|
||||||
|
cell.fill = {
|
||||||
|
type: 'pattern',
|
||||||
|
pattern: 'solid',
|
||||||
|
fgColor: { argb: 'FFF2F2F2' },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 门店列表导出 */
|
||||||
|
export async function exportStoreListExcel(
|
||||||
|
rows: StoreExportRow[],
|
||||||
|
sheetName = '门店仓库',
|
||||||
|
): Promise<ArrayBuffer> {
|
||||||
|
const workbook = new ExcelJS.Workbook();
|
||||||
|
const sheet = workbook.addWorksheet(sheetName);
|
||||||
|
sheet.columns = [
|
||||||
|
{ width: 10 },
|
||||||
|
{ width: 14 },
|
||||||
|
{ width: 18 },
|
||||||
|
{ width: 12 },
|
||||||
|
{ width: 10 },
|
||||||
|
{ width: 12 },
|
||||||
|
{ width: 10 },
|
||||||
|
{ width: 12 },
|
||||||
|
{ width: 14 },
|
||||||
|
{ width: 18 },
|
||||||
|
];
|
||||||
|
styleHeader(sheet.addRow([...LIST_HEADERS]));
|
||||||
|
rows.forEach((item, index) => {
|
||||||
|
const row = sheet.addRow([]);
|
||||||
|
const values = [
|
||||||
|
String(item.drug_id ?? ''),
|
||||||
|
String(item.drug_number ?? ''),
|
||||||
|
String(item.drug_name ?? ''),
|
||||||
|
String(item.pinyin_simple ?? ''),
|
||||||
|
String(item.unit_name ?? ''),
|
||||||
|
String(item.type_text ?? ''),
|
||||||
|
String(item.status_text ?? ''),
|
||||||
|
priceToCellText(item.buy_price),
|
||||||
|
priceToCellText(item.price),
|
||||||
|
String(item.created_at ?? ''),
|
||||||
|
];
|
||||||
|
values.forEach((text, i) => forceTextCell(row.getCell(i + 1), text));
|
||||||
|
styleDataRowFill(row, index);
|
||||||
|
});
|
||||||
|
sheet.views = [{ state: 'frozen', ySplit: 1 }];
|
||||||
|
const buffer = await workbook.xlsx.writeBuffer();
|
||||||
|
return buffer as ArrayBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 门店改价模板(预览弹窗) */
|
||||||
|
export async function exportStorePriceExcel(
|
||||||
|
items: Array<{
|
||||||
|
drug_id?: number | string;
|
||||||
|
drug_number?: string;
|
||||||
|
drug_name?: string;
|
||||||
|
pinyin_simple?: string;
|
||||||
|
unit_name?: string;
|
||||||
|
price?: string | number;
|
||||||
|
}>,
|
||||||
|
sheetName = '门店改价',
|
||||||
|
): Promise<ArrayBuffer> {
|
||||||
|
const workbook = new ExcelJS.Workbook();
|
||||||
|
const sheet = workbook.addWorksheet(sheetName);
|
||||||
|
sheet.columns = [
|
||||||
|
{ width: 10 },
|
||||||
|
{ width: 14 },
|
||||||
|
{ width: 18 },
|
||||||
|
{ width: 12 },
|
||||||
|
{ width: 10 },
|
||||||
|
{ width: 14 },
|
||||||
|
];
|
||||||
|
styleHeader(sheet.addRow([...PRICE_HEADERS]));
|
||||||
|
items.forEach((item, index) => {
|
||||||
|
const row = sheet.addRow([]);
|
||||||
|
const values = [
|
||||||
|
String(item.drug_id ?? ''),
|
||||||
|
String(item.drug_number ?? ''),
|
||||||
|
String(item.drug_name ?? ''),
|
||||||
|
String(item.pinyin_simple ?? ''),
|
||||||
|
String(item.unit_name ?? ''),
|
||||||
|
priceToCellText(item.price),
|
||||||
|
];
|
||||||
|
values.forEach((text, i) => forceTextCell(row.getCell(i + 1), text));
|
||||||
|
styleDataRowFill(row, index);
|
||||||
|
});
|
||||||
|
sheet.views = [{ state: 'frozen', ySplit: 1 }];
|
||||||
|
const buffer = await workbook.xlsx.writeBuffer();
|
||||||
|
return buffer as ArrayBuffer;
|
||||||
|
}
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
/**
|
||||||
|
* 门店批量改价 Excel 解析(ExcelJS)
|
||||||
|
* 只认药品ID + 当前零售价;若含单位列则标记 hasUnitColumn,由上层丢弃并提示
|
||||||
|
* 价格优先读单元格原始字符串,避免 IEEE 浮点
|
||||||
|
*/
|
||||||
|
import ExcelJS from 'exceljs';
|
||||||
|
|
||||||
|
import { roundPrice4 } from '../../admin/utils/priceCompare';
|
||||||
|
|
||||||
|
const MAX_ROWS = 5000;
|
||||||
|
|
||||||
|
export type ParsedStorePriceRow = {
|
||||||
|
drug_id: number;
|
||||||
|
drug_name: string;
|
||||||
|
drug_number: string;
|
||||||
|
price: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ParseStorePriceResult = {
|
||||||
|
items: ParsedStorePriceRow[];
|
||||||
|
invalidCount: number;
|
||||||
|
rowCount: number;
|
||||||
|
/** Excel 是否包含单位列(门店导入须忽略) */
|
||||||
|
hasUnitColumn: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeCell(value: ExcelJS.CellValue): string {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (typeof value === 'object' && value && 'text' in value) {
|
||||||
|
return String((value as { text?: string }).text ?? '').trim();
|
||||||
|
}
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
return Number.isInteger(value) ? String(value) : String(value).trim();
|
||||||
|
}
|
||||||
|
return String(value).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeHeader(value: string): string {
|
||||||
|
return value.replace(/\s+/g, '').replace(/\u3000/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function cellRawString(cell: ExcelJS.Cell): string {
|
||||||
|
const v = cell.value;
|
||||||
|
if (v === null || v === undefined) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (typeof v === 'string') {
|
||||||
|
return v.trim().replace(/,/g, '').replace(/¥/g, '');
|
||||||
|
}
|
||||||
|
if (typeof v === 'number') {
|
||||||
|
return String(v);
|
||||||
|
}
|
||||||
|
if (typeof v === 'object') {
|
||||||
|
if ('result' in v && v.result !== null && v.result !== undefined) {
|
||||||
|
return String((v as { result: unknown }).result).trim();
|
||||||
|
}
|
||||||
|
if ('text' in v && (v as { text?: string }).text != null) {
|
||||||
|
return String((v as { text?: string }).text).trim();
|
||||||
|
}
|
||||||
|
if ('richText' in v && Array.isArray((v as { richText: { text: string }[] }).richText)) {
|
||||||
|
return (v as { richText: { text: string }[] }).richText
|
||||||
|
.map((p) => p.text)
|
||||||
|
.join('')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return String(cell.text ?? '').trim().replace(/,/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
const DRUG_ID_ALIASES = ['药品ID', '药品Id', 'drug_id', 'DrugId'];
|
||||||
|
const NUMBER_ALIASES = ['ErpCode', '商品编码', '药品编码', 'drug_number'];
|
||||||
|
const NAME_ALIASES = ['药名', '通用名', '药品名称', 'drug_name'];
|
||||||
|
const PRICE_ALIASES = ['当前零售价', '零售价', '建议售价', '每克零售价', 'price'];
|
||||||
|
const UNIT_ALIASES = ['单位', '计量单位', 'unit', 'unit_name'];
|
||||||
|
|
||||||
|
function buildHeaderIndexMap(headerRow: ExcelJS.Row): Record<string, number> {
|
||||||
|
const map: Record<string, number> = {};
|
||||||
|
headerRow.eachCell({ includeEmpty: true }, (cell, colNumber) => {
|
||||||
|
const key = normalizeHeader(normalizeCell(cell.value));
|
||||||
|
if (key) {
|
||||||
|
map[key] = colNumber;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickColumn(
|
||||||
|
headerMap: Record<string, number>,
|
||||||
|
aliases: string[],
|
||||||
|
): number | undefined {
|
||||||
|
for (const alias of aliases) {
|
||||||
|
const col = headerMap[normalizeHeader(alias)];
|
||||||
|
if (col) {
|
||||||
|
return col;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCellNumber(row: ExcelJS.Row, col?: number): number {
|
||||||
|
if (!col) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const raw = cellRawString(row.getCell(col));
|
||||||
|
if (raw === '' || !Number.isFinite(Number(raw))) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return roundPrice4(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCellText(row: ExcelJS.Row, col?: number): string {
|
||||||
|
if (!col) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const cell = row.getCell(col);
|
||||||
|
const fromValue = normalizeCell(cell.value);
|
||||||
|
if (fromValue) {
|
||||||
|
return fromValue;
|
||||||
|
}
|
||||||
|
return String(cell.text ?? '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function rowIsEmpty(row: ExcelJS.Row): boolean {
|
||||||
|
let hasValue = false;
|
||||||
|
row.eachCell({ includeEmpty: false }, (cell) => {
|
||||||
|
if (normalizeCell(cell.value) !== '') {
|
||||||
|
hasValue = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return !hasValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function parseStorePriceExcelBuffer(
|
||||||
|
buffer: ArrayBuffer,
|
||||||
|
): Promise<ParseStorePriceResult> {
|
||||||
|
const workbook = new ExcelJS.Workbook();
|
||||||
|
await workbook.xlsx.load(buffer);
|
||||||
|
|
||||||
|
const sheet = workbook.worksheets[0];
|
||||||
|
if (!sheet) {
|
||||||
|
return { items: [], invalidCount: 0, rowCount: 0, hasUnitColumn: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const headerRow = sheet.getRow(1);
|
||||||
|
const headerMap = buildHeaderIndexMap(headerRow);
|
||||||
|
|
||||||
|
const drugIdCol = pickColumn(headerMap, DRUG_ID_ALIASES);
|
||||||
|
const numberCol = pickColumn(headerMap, NUMBER_ALIASES);
|
||||||
|
const nameCol = pickColumn(headerMap, NAME_ALIASES);
|
||||||
|
const priceCol = pickColumn(headerMap, PRICE_ALIASES);
|
||||||
|
const unitCol = pickColumn(headerMap, UNIT_ALIASES);
|
||||||
|
const hasUnitColumn = !!unitCol;
|
||||||
|
|
||||||
|
const items: ParsedStorePriceRow[] = [];
|
||||||
|
let invalidCount = 0;
|
||||||
|
const rowCount = Math.min(sheet.rowCount, MAX_ROWS + 1);
|
||||||
|
|
||||||
|
for (let rowIndex = 2; rowIndex <= rowCount; rowIndex++) {
|
||||||
|
const row = sheet.getRow(rowIndex);
|
||||||
|
if (rowIsEmpty(row)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const drugIdRaw = getCellText(row, drugIdCol);
|
||||||
|
const drugId = Number.parseInt(drugIdRaw, 10) || 0;
|
||||||
|
const drugNumber = getCellText(row, numberCol);
|
||||||
|
const drugName = getCellText(row, nameCol);
|
||||||
|
const price = getCellNumber(row, priceCol);
|
||||||
|
|
||||||
|
if (drugId <= 0 && !drugName && !drugNumber && price === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (drugId <= 0) {
|
||||||
|
invalidCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
drug_id: drugId,
|
||||||
|
drug_name: drugName,
|
||||||
|
drug_number: drugNumber,
|
||||||
|
price,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
items,
|
||||||
|
invalidCount,
|
||||||
|
rowCount: Math.max(0, rowCount - 1),
|
||||||
|
hasUnitColumn,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -88,7 +88,7 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
formApi.setValues({ sign_image: data.signature });
|
formApi.setValues({ sign_image: data.signature });
|
||||||
} else {
|
} else {
|
||||||
currentSignature.value = '';
|
currentSignature.value = '';
|
||||||
formApi.resetFields();
|
formApi.resetForm();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -20,19 +20,12 @@ import {
|
|||||||
getDepartmentFinanceReportDetailApi,
|
getDepartmentFinanceReportDetailApi,
|
||||||
getDepartmentFinanceStoreDrugDetailApi,
|
getDepartmentFinanceStoreDrugDetailApi,
|
||||||
} from '#/views/finance/department-finance/api';
|
} from '#/views/finance/department-finance/api';
|
||||||
|
import {
|
||||||
|
MEDICINE_SCOPE_NOTE,
|
||||||
|
STORE_METRIC_FIELDS,
|
||||||
|
} from '#/views/finance/department-finance/config/metrics';
|
||||||
import { buildAndDownloadDepartmentFinanceExcel } from '#/views/finance/department-finance/utils/exportDepartmentFinanceExcel';
|
import { buildAndDownloadDepartmentFinanceExcel } from '#/views/finance/department-finance/utils/exportDepartmentFinanceExcel';
|
||||||
|
|
||||||
/** 诊所卡内展示的金额字段(不含开票合计,单独强调) */
|
|
||||||
const STORE_METRIC_FIELDS = [
|
|
||||||
{ key: 'herbal_supply_price', label: '中药供货价' },
|
|
||||||
{ key: 'granule_supply_price', label: '颗粒药供货价' },
|
|
||||||
{ key: 'medicine_supply_price', label: '西药供货价' },
|
|
||||||
{ key: 'herbal_decoct_fee', label: '中药代煎费' },
|
|
||||||
{ key: 'herbal_express_fee', label: '中药运费' },
|
|
||||||
{ key: 'medicine_express_fee', label: '西药运费' },
|
|
||||||
{ key: 'granule_express_fee', label: '颗粒药运费' },
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const exportLoading = ref(false);
|
const exportLoading = ref(false);
|
||||||
const reportId = ref(0);
|
const reportId = ref(0);
|
||||||
@@ -68,12 +61,13 @@ function formatMoney(value: unknown): string {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 英雄头下方 7 项指标(开票合计已在头区突出,不再重复) */
|
/** 英雄头下方指标(开票合计已在头区突出,不再重复) */
|
||||||
const metricItems = computed(() => {
|
const metricItems = computed(() => {
|
||||||
const s = detail.value?.summary;
|
const s = detail.value?.summary;
|
||||||
if (!s) return [];
|
if (!s) return [];
|
||||||
return STORE_METRIC_FIELDS.map((f) => ({
|
return STORE_METRIC_FIELDS.map((f) => ({
|
||||||
label: f.label,
|
label: f.label,
|
||||||
|
tip: 'tip' in f ? f.tip : '',
|
||||||
value: s[f.key],
|
value: s[f.key],
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
@@ -207,14 +201,20 @@ defineExpose({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 指标卡:7 项费用 -->
|
<!-- 口径说明:西药侧含非中药,已取消颗粒栏目 -->
|
||||||
|
<div class="df-detail-note">{{ MEDICINE_SCOPE_NOTE }}</div>
|
||||||
|
|
||||||
|
<!-- 指标卡 -->
|
||||||
<div v-if="metricItems.length" class="df-detail-metrics">
|
<div v-if="metricItems.length" class="df-detail-metrics">
|
||||||
<div
|
<div
|
||||||
v-for="item in metricItems"
|
v-for="item in metricItems"
|
||||||
:key="item.label"
|
:key="item.label"
|
||||||
class="df-detail-metric"
|
class="df-detail-metric"
|
||||||
>
|
>
|
||||||
<div class="df-detail-metric__label">{{ item.label }}</div>
|
<div class="df-detail-metric__label">
|
||||||
|
{{ item.label }}
|
||||||
|
<span v-if="item.tip" class="df-detail-metric__tip">{{ item.tip }}</span>
|
||||||
|
</div>
|
||||||
<div class="df-detail-metric__value">
|
<div class="df-detail-metric__value">
|
||||||
¥{{ formatMoney(item.value) }}
|
¥{{ formatMoney(item.value) }}
|
||||||
</div>
|
</div>
|
||||||
@@ -240,7 +240,12 @@ defineExpose({
|
|||||||
:key="field.key"
|
:key="field.key"
|
||||||
class="df-detail-store__metric"
|
class="df-detail-store__metric"
|
||||||
>
|
>
|
||||||
<span>{{ field.label }}</span>
|
<span>
|
||||||
|
{{ field.label }}
|
||||||
|
<template v-if="'tip' in field && field.tip">
|
||||||
|
({{ field.tip }})
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
<span>¥{{ formatMoney(store[field.key]) }}</span>
|
<span>¥{{ formatMoney(store[field.key]) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -343,6 +348,28 @@ defineExpose({
|
|||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
box-shadow: 0 1px 2px rgb(15 23 42 / 4%);
|
box-shadow: 0 1px 2px rgb(15 23 42 / 4%);
|
||||||
}
|
}
|
||||||
|
.df-detail-note {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #475569;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.dark .df-detail-note {
|
||||||
|
color: #cbd5e1;
|
||||||
|
background: #0b1220;
|
||||||
|
border-color: #1e293b;
|
||||||
|
}
|
||||||
|
.df-detail-metric__tip {
|
||||||
|
display: block;
|
||||||
|
margin-top: 2px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
.dark .df-detail-hero {
|
.dark .df-detail-hero {
|
||||||
background: #0f172a;
|
background: #0f172a;
|
||||||
border-color: #334155;
|
border-color: #334155;
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
/**
|
||||||
|
* 部门财务指标字段与口径文案
|
||||||
|
* 西药侧含服务包、保健食品等非中药;已取消独立颗粒栏目
|
||||||
|
*/
|
||||||
|
export const MEDICINE_SCOPE_NOTE =
|
||||||
|
'口径说明:西药供货价/运费含服务包、保健食品、医疗器械、非药品等所有非中药;已取消独立「颗粒药」栏目。';
|
||||||
|
|
||||||
|
/** 诊所卡/详情展示的金额字段(不含开票合计) */
|
||||||
|
export const STORE_METRIC_FIELDS = [
|
||||||
|
{ key: 'herbal_supply_price', label: '中药供货价' },
|
||||||
|
{
|
||||||
|
key: 'medicine_supply_price',
|
||||||
|
label: '西药供货价',
|
||||||
|
tip: '含服务包、保健食品等非中药',
|
||||||
|
},
|
||||||
|
{ key: 'herbal_decoct_fee', label: '中药代煎费' },
|
||||||
|
{ key: 'herbal_express_fee', label: '中药运费' },
|
||||||
|
{
|
||||||
|
key: 'medicine_express_fee',
|
||||||
|
label: '西药运费',
|
||||||
|
tip: '含非中药订单运费',
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
/** Excel 汇总指标 */
|
||||||
|
export const SUMMARY_METRICS: {
|
||||||
|
label: string;
|
||||||
|
key: string;
|
||||||
|
highlight?: boolean;
|
||||||
|
}[] = [
|
||||||
|
{ label: '中药供货价', key: 'herbal_supply_price' },
|
||||||
|
{
|
||||||
|
label: '西药供货价(含服务包/保健食品等非中药)',
|
||||||
|
key: 'medicine_supply_price',
|
||||||
|
},
|
||||||
|
{ label: '中药代煎费', key: 'herbal_decoct_fee' },
|
||||||
|
{ label: '中药运费', key: 'herbal_express_fee' },
|
||||||
|
{ label: '西药运费(含非中药)', key: 'medicine_express_fee' },
|
||||||
|
{ label: '开票合计', key: 'invoice_total', highlight: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const STORE_HEADERS = [
|
||||||
|
'诊所名称',
|
||||||
|
'中药供货价',
|
||||||
|
'西药供货价(含服务包/保健食品等非中药)',
|
||||||
|
'中药代煎费',
|
||||||
|
'中药运费',
|
||||||
|
'西药运费(含非中药)',
|
||||||
|
'开票合计',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const STORE_MONEY_KEYS = [
|
||||||
|
'herbal_supply_price',
|
||||||
|
'medicine_supply_price',
|
||||||
|
'herbal_decoct_fee',
|
||||||
|
'herbal_express_fee',
|
||||||
|
'medicine_express_fee',
|
||||||
|
'invoice_total',
|
||||||
|
] as const;
|
||||||
@@ -9,6 +9,12 @@ import {
|
|||||||
getDepartmentFinanceReportDetailApi,
|
getDepartmentFinanceReportDetailApi,
|
||||||
getDepartmentFinanceStoreDrugDetailApi,
|
getDepartmentFinanceStoreDrugDetailApi,
|
||||||
} from '#/views/finance/department-finance/api';
|
} from '#/views/finance/department-finance/api';
|
||||||
|
import {
|
||||||
|
MEDICINE_SCOPE_NOTE,
|
||||||
|
STORE_HEADERS,
|
||||||
|
STORE_MONEY_KEYS,
|
||||||
|
SUMMARY_METRICS,
|
||||||
|
} from '#/views/finance/department-finance/config/metrics';
|
||||||
|
|
||||||
const XLSX_MIME =
|
const XLSX_MIME =
|
||||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||||
@@ -25,30 +31,6 @@ const DOUBLE_TOP_BORDER: Partial<ExcelJS.Borders> = {
|
|||||||
top: { style: 'double', color: { argb: 'FF000000' } },
|
top: { style: 'double', color: { argb: 'FF000000' } },
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 诊所明细金额列(不含诊所名称) */
|
|
||||||
const STORE_MONEY_KEYS = [
|
|
||||||
'herbal_supply_price',
|
|
||||||
'granule_supply_price',
|
|
||||||
'medicine_supply_price',
|
|
||||||
'herbal_decoct_fee',
|
|
||||||
'herbal_express_fee',
|
|
||||||
'medicine_express_fee',
|
|
||||||
'granule_express_fee',
|
|
||||||
'invoice_total',
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
const STORE_HEADERS = [
|
|
||||||
'诊所名称',
|
|
||||||
'中药供货价',
|
|
||||||
'颗粒药供货价',
|
|
||||||
'西药供货价',
|
|
||||||
'中药代煎费',
|
|
||||||
'中药运费',
|
|
||||||
'西药运费',
|
|
||||||
'颗粒药运费',
|
|
||||||
'开票合计',
|
|
||||||
];
|
|
||||||
|
|
||||||
const DRUG_HEADERS = [
|
const DRUG_HEADERS = [
|
||||||
'诊所名称',
|
'诊所名称',
|
||||||
'药品编码',
|
'药品编码',
|
||||||
@@ -59,18 +41,6 @@ const DRUG_HEADERS = [
|
|||||||
'供货总额',
|
'供货总额',
|
||||||
];
|
];
|
||||||
|
|
||||||
/** 汇总键值表:指标名 -> summary 字段 */
|
|
||||||
const SUMMARY_METRICS: { label: string; key: string; highlight?: boolean }[] = [
|
|
||||||
{ label: '中药供货价', key: 'herbal_supply_price' },
|
|
||||||
{ label: '颗粒药供货价', key: 'granule_supply_price' },
|
|
||||||
{ label: '西药供货价', key: 'medicine_supply_price' },
|
|
||||||
{ label: '中药代煎费', key: 'herbal_decoct_fee' },
|
|
||||||
{ label: '中药运费', key: 'herbal_express_fee' },
|
|
||||||
{ label: '西药运费', key: 'medicine_express_fee' },
|
|
||||||
{ label: '颗粒药运费', key: 'granule_express_fee' },
|
|
||||||
{ label: '开票合计', key: 'invoice_total', highlight: true },
|
|
||||||
];
|
|
||||||
|
|
||||||
export interface DepartmentFinanceDrugRow {
|
export interface DepartmentFinanceDrugRow {
|
||||||
store_name: string;
|
store_name: string;
|
||||||
drug_number?: string;
|
drug_number?: string;
|
||||||
@@ -288,10 +258,19 @@ function buildSummarySheet(
|
|||||||
labelCell.fill = zebra;
|
labelCell.fill = zebra;
|
||||||
moneyCell.fill = zebra;
|
moneyCell.fill = zebra;
|
||||||
}
|
}
|
||||||
sheet.getRow(dataRow).height = metric.highlight ? 28 : 24;
|
sheet.getRow(dataRow).height = 24;
|
||||||
dataRow += 1;
|
dataRow += 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 口径说明行
|
||||||
|
sheet.mergeCells(dataRow, 1, dataRow, 2);
|
||||||
|
const noteCell = sheet.getCell(dataRow, 1);
|
||||||
|
noteCell.value = MEDICINE_SCOPE_NOTE;
|
||||||
|
noteCell.font = { size: 10, color: { argb: 'FF64748B' } };
|
||||||
|
noteCell.alignment = { wrapText: true, vertical: 'middle' };
|
||||||
|
sheet.getRow(dataRow).height = 36;
|
||||||
|
dataRow += 1;
|
||||||
|
|
||||||
autoFitColumns(sheet, layoutCols, 1, dataRow - 1, 14, 28);
|
autoFitColumns(sheet, layoutCols, 1, dataRow - 1, 14, 28);
|
||||||
freezeAt(sheet, headerRow);
|
freezeAt(sheet, headerRow);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,12 +32,22 @@ const statistics = computed(() => {
|
|||||||
return [
|
return [
|
||||||
{ title: '开票合计', value: m.invoice_total, icon: 'mdi:cash-multiple', color: 'text-blue-600', unit: '¥' },
|
{ title: '开票合计', value: m.invoice_total, icon: 'mdi:cash-multiple', color: 'text-blue-600', unit: '¥' },
|
||||||
{ title: '中药供货价', value: m.herbal_supply_price, icon: 'mdi:leaf', color: 'text-green-600', unit: '¥' },
|
{ title: '中药供货价', value: m.herbal_supply_price, icon: 'mdi:leaf', color: 'text-green-600', unit: '¥' },
|
||||||
{ title: '颗粒药供货价', value: m.granule_supply_price, icon: 'mdi:pill', color: 'text-teal-600', unit: '¥' },
|
{
|
||||||
{ title: '西药供货价', value: m.medicine_supply_price, icon: 'mdi:pharmacy', color: 'text-purple-600', unit: '¥' },
|
title: '西药供货价',
|
||||||
|
value: Number(m.medicine_supply_price || 0) + Number(m.granule_supply_price || 0),
|
||||||
|
icon: 'mdi:pharmacy',
|
||||||
|
color: 'text-purple-600',
|
||||||
|
unit: '¥',
|
||||||
|
},
|
||||||
{ title: '中药代煎费', value: m.herbal_decoct_fee, icon: 'mdi:fire', color: 'text-orange-600', unit: '¥' },
|
{ title: '中药代煎费', value: m.herbal_decoct_fee, icon: 'mdi:fire', color: 'text-orange-600', unit: '¥' },
|
||||||
{ title: '中药运费', value: m.herbal_express_fee, icon: 'skill-icons:expressjs-dark', color: 'text-orange-500', unit: '¥' },
|
{ title: '中药运费', value: m.herbal_express_fee, icon: 'skill-icons:expressjs-dark', color: 'text-orange-500', unit: '¥' },
|
||||||
{ title: '西药运费', value: m.medicine_express_fee, icon: 'mdi:truck', color: 'text-indigo-600', unit: '¥' },
|
{
|
||||||
{ title: '颗粒药运费', value: m.granule_express_fee, icon: 'mdi:truck-outline', color: 'text-cyan-600', unit: '¥' },
|
title: '西药运费',
|
||||||
|
value: Number(m.medicine_express_fee || 0) + Number(m.granule_express_fee || 0),
|
||||||
|
icon: 'mdi:truck',
|
||||||
|
color: 'text-indigo-600',
|
||||||
|
unit: '¥',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -93,6 +103,9 @@ defineExpose({
|
|||||||
统计区间:{{ searchTime[0] }} ~ {{ searchTime[1] }}
|
统计区间:{{ searchTime[0] }} ~ {{ searchTime[1] }}
|
||||||
<Tag color="blue" class="ml-2">诊所 ID: {{ storeId }}</Tag>
|
<Tag color="blue" class="ml-2">诊所 ID: {{ storeId }}</Tag>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mb-3 rounded border border-slate-200 bg-slate-50 px-3 py-2 text-xs text-slate-600">
|
||||||
|
口径说明:西药供货价/运费含服务包、保健食品、医疗器械、非药品等所有非中药;已取消独立「颗粒药」栏目。
|
||||||
|
</div>
|
||||||
<StatisticsReconciliation
|
<StatisticsReconciliation
|
||||||
v-if="statistics"
|
v-if="statistics"
|
||||||
:statistics="statistics"
|
:statistics="statistics"
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
const prefix = 'account-able-change-log/';
|
||||||
|
|
||||||
|
/** 平台审计全量列表(audit=1) */
|
||||||
|
export async function getAccountAbleChangeLogAuditList(data: any) {
|
||||||
|
return requestClient.get<any>(`${prefix}list`, {
|
||||||
|
params: { ...data, audit: 1 },
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import type { VbenFormProps } from '#/adapter/form';
|
||||||
|
|
||||||
|
export const formOptions: VbenFormProps = {
|
||||||
|
collapsed: false,
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: { placeholder: '订单号', allowClear: true },
|
||||||
|
fieldName: 'order_no',
|
||||||
|
label: '订单号',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: { placeholder: '主体用户ID', allowClear: true },
|
||||||
|
fieldName: 'user_id',
|
||||||
|
label: '用户ID',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
allowClear: true,
|
||||||
|
options: [
|
||||||
|
{ label: '诊所', value: 1 },
|
||||||
|
{ label: '平台', value: 2 },
|
||||||
|
{ label: '供应商', value: 3 },
|
||||||
|
],
|
||||||
|
placeholder: '全部',
|
||||||
|
},
|
||||||
|
fieldName: 'user_type',
|
||||||
|
label: '用户类型',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: { placeholder: '来源类型 settle/withdraw…', allowClear: true },
|
||||||
|
fieldName: 'source_type',
|
||||||
|
label: '来源类型',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'RangePicker',
|
||||||
|
componentProps: {
|
||||||
|
format: 'YYYY-MM-DD HH:mm:ss',
|
||||||
|
showTime: true,
|
||||||
|
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||||
|
},
|
||||||
|
fieldName: 'search_time',
|
||||||
|
label: '时间范围',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
showCollapseButton: true,
|
||||||
|
submitButtonOptions: { content: '查询' },
|
||||||
|
submitOnChange: false,
|
||||||
|
submitOnEnter: true,
|
||||||
|
};
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { getAccountAbleChangeLogAuditList } from '../api';
|
||||||
|
|
||||||
|
export const gridOptions: VxeGridProps<any> = {
|
||||||
|
columns: [
|
||||||
|
{ field: 'id', title: 'ID', width: 70 },
|
||||||
|
{ field: 'created_at', title: '时间', width: 170 },
|
||||||
|
{ field: 'user_type_txt', title: '用户类型', width: 90 },
|
||||||
|
{ field: 'user_id', title: '用户ID', width: 90 },
|
||||||
|
{ field: 'source_type_txt', title: '来源', width: 100 },
|
||||||
|
{ field: 'field_name_txt', title: '字段', width: 140 },
|
||||||
|
{ field: 'before_amount', title: '变动前', width: 110 },
|
||||||
|
{ field: 'after_amount', title: '变动后', width: 110 },
|
||||||
|
{ field: 'change_amount', title: '变动值', width: 110 },
|
||||||
|
{ field: 'order_no', title: '订单号', minWidth: 140 },
|
||||||
|
{ field: 'source_table_txt', title: '来源表', width: 120 },
|
||||||
|
{ field: 'remark', title: '备注', minWidth: 140 },
|
||||||
|
],
|
||||||
|
keepSource: true,
|
||||||
|
pagerConfig: {},
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
// 走 BaseService search_time(勿转 start_time),并带 audit=1
|
||||||
|
query: async ({ page }, formValues) =>
|
||||||
|
getAccountAbleChangeLogAuditList({
|
||||||
|
page: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
...formValues,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
height: 'auto',
|
||||||
|
toolbarConfig: { search: true, refresh: true, zoom: true },
|
||||||
|
};
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
/**
|
||||||
|
* 账户可提现金额变动审计(平台全量,audit=1)
|
||||||
|
*/
|
||||||
|
import { Page } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { formOptions } from './config/search';
|
||||||
|
import { gridOptions } from './config/table';
|
||||||
|
|
||||||
|
defineOptions({ name: 'AccountAbleChangeLog' });
|
||||||
|
|
||||||
|
const [Grid] = useVbenVxeGrid({ formOptions, gridOptions });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height title="账户变动审计">
|
||||||
|
<Grid />
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
11
apps/web-antd/src/views/log/error-log/api/index.ts
Normal file
11
apps/web-antd/src/views/log/error-log/api/index.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
const prefix = 'error-log/';
|
||||||
|
|
||||||
|
export async function getErrorLogList(data: any) {
|
||||||
|
return requestClient.get<any>(`${prefix}list`, { params: data });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getErrorLogDetail(id: number) {
|
||||||
|
return requestClient.get<any>(`${prefix}detail`, { params: { id } });
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
/**
|
||||||
|
* 错误日志详情
|
||||||
|
*/
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Descriptions, Tag } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { getErrorLogDetail } from '../api';
|
||||||
|
|
||||||
|
const data = ref<Record<string, any>>({});
|
||||||
|
const contentText = computed(() => {
|
||||||
|
const c = data.value.content;
|
||||||
|
if (c == null) return '(无)';
|
||||||
|
try {
|
||||||
|
return typeof c === 'string' ? c : JSON.stringify(c, null, 2);
|
||||||
|
} catch {
|
||||||
|
return String(c);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
showConfirmButton: false,
|
||||||
|
cancelText: '关闭',
|
||||||
|
draggable: true,
|
||||||
|
onCancel() {
|
||||||
|
modalApi.close();
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen) {
|
||||||
|
if (!isOpen) {
|
||||||
|
data.value = {};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const payload = modalApi.getData<{ id?: number; values?: any }>();
|
||||||
|
const id = Number(payload?.id || payload?.values?.id || 0);
|
||||||
|
if (id <= 0) {
|
||||||
|
data.value = payload?.values || {};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.setState({ loading: true });
|
||||||
|
try {
|
||||||
|
const res = await getErrorLogDetail(id);
|
||||||
|
data.value = res?.data || res || {};
|
||||||
|
} catch {
|
||||||
|
data.value = payload?.values || {};
|
||||||
|
} finally {
|
||||||
|
modalApi.setState({ loading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal class="w-[90%] md:w-[720px]" title="错误日志详情">
|
||||||
|
<Descriptions bordered :column="1" size="small">
|
||||||
|
<Descriptions.Item label="ID">{{ data.id }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="类型">
|
||||||
|
<Tag>{{ data.type_txt || data.type }}</Tag>
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="时间">{{ data.created_at }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="内容">
|
||||||
|
<pre class="max-h-80 overflow-auto whitespace-pre-wrap break-all text-xs">{{ contentText }}</pre>
|
||||||
|
</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
27
apps/web-antd/src/views/log/error-log/config/search.ts
Normal file
27
apps/web-antd/src/views/log/error-log/config/search.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import type { VbenFormProps } from '#/adapter/form';
|
||||||
|
|
||||||
|
import { logTimeRangeSchema } from '../../shared/normalizeLogFilters';
|
||||||
|
|
||||||
|
export const formOptions: VbenFormProps = {
|
||||||
|
collapsed: false,
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
allowClear: true,
|
||||||
|
options: [
|
||||||
|
{ label: '业务', value: 0 },
|
||||||
|
{ label: '消息队列', value: 1 },
|
||||||
|
],
|
||||||
|
placeholder: '全部',
|
||||||
|
},
|
||||||
|
fieldName: 'type',
|
||||||
|
label: '类型',
|
||||||
|
},
|
||||||
|
{ ...logTimeRangeSchema },
|
||||||
|
],
|
||||||
|
showCollapseButton: true,
|
||||||
|
submitButtonOptions: { content: '查询' },
|
||||||
|
submitOnChange: false,
|
||||||
|
submitOnEnter: true,
|
||||||
|
};
|
||||||
28
apps/web-antd/src/views/log/error-log/config/table.ts
Normal file
28
apps/web-antd/src/views/log/error-log/config/table.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { normalizeLogTimeFilters } from '../../shared/normalizeLogFilters';
|
||||||
|
import { getErrorLogList } from '../api';
|
||||||
|
|
||||||
|
export const gridOptions: VxeGridProps<any> = {
|
||||||
|
columns: [
|
||||||
|
{ field: 'id', title: 'ID', width: 80 },
|
||||||
|
{ field: 'type_txt', title: '类型', width: 100, slots: { default: 'type' } },
|
||||||
|
{ field: 'created_at', title: '创建时间', width: 170 },
|
||||||
|
{ field: 'content', title: '内容摘要', minWidth: 280, slots: { default: 'content' } },
|
||||||
|
{ title: '操作', width: 90, fixed: 'right', slots: { default: 'action' } },
|
||||||
|
],
|
||||||
|
keepSource: true,
|
||||||
|
pagerConfig: {},
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) =>
|
||||||
|
getErrorLogList({
|
||||||
|
page: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
...normalizeLogTimeFilters(formValues || {}),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
height: 'auto',
|
||||||
|
toolbarConfig: { search: true, refresh: true, zoom: true },
|
||||||
|
};
|
||||||
59
apps/web-antd/src/views/log/error-log/index.vue
Normal file
59
apps/web-antd/src/views/log/error-log/index.vue
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
/**
|
||||||
|
* 错误日志列表(日志审计)
|
||||||
|
*/
|
||||||
|
import { Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Tag } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import { TableAction } from '#/components/table-action';
|
||||||
|
|
||||||
|
import DetailModal from './components/detail-modal.vue';
|
||||||
|
import { formOptions } from './config/search';
|
||||||
|
import { gridOptions } from './config/table';
|
||||||
|
|
||||||
|
defineOptions({ name: 'ErrorLog' });
|
||||||
|
|
||||||
|
const [Grid] = useVbenVxeGrid({ formOptions, gridOptions });
|
||||||
|
const [DetailModalComp, detailModalApi] = useVbenModal({
|
||||||
|
connectedComponent: DetailModal,
|
||||||
|
});
|
||||||
|
|
||||||
|
function openDetail(row: any) {
|
||||||
|
detailModalApi.setData({ id: Number(row?.id || 0), values: row });
|
||||||
|
detailModalApi.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
function preview(content: unknown) {
|
||||||
|
try {
|
||||||
|
const s = typeof content === 'string' ? content : JSON.stringify(content);
|
||||||
|
return s.length > 120 ? `${s.slice(0, 120)}…` : s;
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height title="错误日志">
|
||||||
|
<DetailModalComp />
|
||||||
|
<Grid>
|
||||||
|
<template #type="{ row }">
|
||||||
|
<Tag :color="Number(row.type) === 1 ? 'orange' : 'blue'">
|
||||||
|
{{ row.type_txt || row.type }}
|
||||||
|
</Tag>
|
||||||
|
</template>
|
||||||
|
<template #content="{ row }">
|
||||||
|
<span class="text-xs text-gray-600">{{ preview(row.content) }}</span>
|
||||||
|
</template>
|
||||||
|
<template #action="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{ label: '详情', type: 'link', size: 'small', onClick: () => openDetail(row) },
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
16
apps/web-antd/src/views/log/idcard-verify-log/api/index.ts
Normal file
16
apps/web-antd/src/views/log/idcard-verify-log/api/index.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
const prefix = 'idcard-verify-log/';
|
||||||
|
|
||||||
|
export async function getIdcardVerifyLogList(data: any) {
|
||||||
|
return requestClient.get<any>(`${prefix}list`, { params: data });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getIdcardVerifyLogDetail(id: number) {
|
||||||
|
return requestClient.get<any>(`${prefix}detail`, { params: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 超管:旧明文姓名批量 MD5 回填 */
|
||||||
|
export async function backfillIdcardVerifyNameHash() {
|
||||||
|
return requestClient.post<any>(`${prefix}backfill-name-hash`);
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Descriptions, Tag } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { getIdcardVerifyLogDetail } from '../api';
|
||||||
|
|
||||||
|
const data = ref<Record<string, any>>({});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
showConfirmButton: false,
|
||||||
|
cancelText: '关闭',
|
||||||
|
draggable: true,
|
||||||
|
onCancel() {
|
||||||
|
modalApi.close();
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen) {
|
||||||
|
if (!isOpen) {
|
||||||
|
data.value = {};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const payload = modalApi.getData<{ id?: number; values?: any }>();
|
||||||
|
const id = Number(payload?.id || payload?.values?.id || 0);
|
||||||
|
if (id <= 0) {
|
||||||
|
data.value = payload?.values || {};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.setState({ loading: true });
|
||||||
|
try {
|
||||||
|
const res = await getIdcardVerifyLogDetail(id);
|
||||||
|
data.value = res?.data || res || {};
|
||||||
|
} catch {
|
||||||
|
data.value = payload?.values || {};
|
||||||
|
} finally {
|
||||||
|
modalApi.setState({ loading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal class="w-[90%] md:w-[640px]" title="身份核验日志详情">
|
||||||
|
<Descriptions bordered :column="1" size="small">
|
||||||
|
<Descriptions.Item label="ID">{{ data.id }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="姓名摘要">{{ data.name || '-' }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="结论">
|
||||||
|
<Tag :color="Number(data.is_match) === 1 ? 'success' : 'error'">
|
||||||
|
{{ data.is_match_txt }}
|
||||||
|
</Tag>
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="场景">{{ data.scene_txt }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="来源">{{ data.source_txt }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="接口码">{{ data.code }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="消息">{{ data.message }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="时间">{{ data.created_at }}</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import type { VbenFormProps } from '#/adapter/form';
|
||||||
|
|
||||||
|
import { logTimeRangeSchema } from '../../shared/normalizeLogFilters';
|
||||||
|
|
||||||
|
export const formOptions: VbenFormProps = {
|
||||||
|
collapsed: false,
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '姓名(精确匹配)',
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '姓名',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
allowClear: true,
|
||||||
|
options: [
|
||||||
|
{ label: '匹配', value: 1 },
|
||||||
|
{ label: '不匹配', value: 0 },
|
||||||
|
],
|
||||||
|
placeholder: '全部',
|
||||||
|
},
|
||||||
|
fieldName: 'is_match',
|
||||||
|
label: '结论',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
allowClear: true,
|
||||||
|
options: [
|
||||||
|
{ label: '就诊人', value: 'patient' },
|
||||||
|
{ label: '医生', value: 'doctor' },
|
||||||
|
{ label: '药师', value: 'pharmacist' },
|
||||||
|
],
|
||||||
|
placeholder: '全部',
|
||||||
|
},
|
||||||
|
fieldName: 'scene',
|
||||||
|
label: '场景',
|
||||||
|
},
|
||||||
|
{ ...logTimeRangeSchema },
|
||||||
|
],
|
||||||
|
showCollapseButton: true,
|
||||||
|
submitButtonOptions: { content: '查询' },
|
||||||
|
submitOnChange: false,
|
||||||
|
submitOnEnter: true,
|
||||||
|
};
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { normalizeLogTimeFilters } from '../../shared/normalizeLogFilters';
|
||||||
|
import { getIdcardVerifyLogList } from '../api';
|
||||||
|
|
||||||
|
export const gridOptions: VxeGridProps<any> = {
|
||||||
|
columns: [
|
||||||
|
{ field: 'id', title: 'ID', width: 80 },
|
||||||
|
{ field: 'name', title: '姓名摘要', minWidth: 280 },
|
||||||
|
{ field: 'is_match_txt', title: '结论', width: 90, slots: { default: 'is_match' } },
|
||||||
|
{ field: 'scene_txt', title: '场景', width: 90 },
|
||||||
|
{ field: 'source_txt', title: '来源', width: 90 },
|
||||||
|
{ field: 'code', title: '接口码', width: 80 },
|
||||||
|
{ field: 'message', title: '消息', minWidth: 160 },
|
||||||
|
{ field: 'created_at', title: '时间', width: 170 },
|
||||||
|
{ title: '操作', width: 90, fixed: 'right', slots: { default: 'action' } },
|
||||||
|
],
|
||||||
|
keepSource: true,
|
||||||
|
pagerConfig: {},
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) =>
|
||||||
|
getIdcardVerifyLogList({
|
||||||
|
page: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
...normalizeLogTimeFilters(formValues || {}),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
height: 'auto',
|
||||||
|
toolbarConfig: { search: true, refresh: true, zoom: true },
|
||||||
|
};
|
||||||
97
apps/web-antd/src/views/log/idcard-verify-log/index.vue
Normal file
97
apps/web-antd/src/views/log/idcard-verify-log/index.vue
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
/**
|
||||||
|
* 身份证实名核验日志(name 存 MD5 摘要;超管可回填旧明文)
|
||||||
|
*/
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
import { useUserStore } from '@vben/stores';
|
||||||
|
|
||||||
|
import { Button, Modal, Tag, message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import { TableAction } from '#/components/table-action';
|
||||||
|
|
||||||
|
import { backfillIdcardVerifyNameHash } from './api';
|
||||||
|
import DetailModal from './components/detail-modal.vue';
|
||||||
|
import { formOptions } from './config/search';
|
||||||
|
import { gridOptions } from './config/table';
|
||||||
|
|
||||||
|
defineOptions({ name: 'IdcardVerifyLog' });
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
/** 仅超管 role_id=1 可见回填按钮,与后端 SUPPER_ADMIN 一致 */
|
||||||
|
const isSuperAdmin = computed(() => {
|
||||||
|
const roleId = Number(
|
||||||
|
userStore.userInfo?.role_id ?? userStore.userInfo?.roles?.id,
|
||||||
|
);
|
||||||
|
return roleId === 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
const backfilling = ref(false);
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({ formOptions, gridOptions });
|
||||||
|
const [DetailModalComp, detailModalApi] = useVbenModal({
|
||||||
|
connectedComponent: DetailModal,
|
||||||
|
});
|
||||||
|
|
||||||
|
function openDetail(row: any) {
|
||||||
|
detailModalApi.setData({ id: Number(row?.id || 0), values: row });
|
||||||
|
detailModalApi.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 超管确认后把历史明文 name 刷成 MD5 摘要 */
|
||||||
|
function handleBackfillNameHash() {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '回填姓名 MD5',
|
||||||
|
content:
|
||||||
|
'将把尚未哈希的明文姓名批量更新为 MD5 摘要。已是 32 位摘要的行会跳过,可重复执行。是否继续?',
|
||||||
|
okText: '开始回填',
|
||||||
|
cancelText: '取消',
|
||||||
|
onOk: async () => {
|
||||||
|
backfilling.value = true;
|
||||||
|
try {
|
||||||
|
const res = await backfillIdcardVerifyNameHash();
|
||||||
|
const data = res?.data || res || {};
|
||||||
|
message.success(
|
||||||
|
`回填完成:更新 ${Number(data.updated || 0)} 条,跳过 ${Number(data.skipped || 0)} 条`,
|
||||||
|
);
|
||||||
|
await gridApi.query();
|
||||||
|
} finally {
|
||||||
|
backfilling.value = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height title="身份证实名核验日志">
|
||||||
|
<DetailModalComp />
|
||||||
|
<Grid>
|
||||||
|
<template #toolbar-tools>
|
||||||
|
<Button
|
||||||
|
v-if="isSuperAdmin"
|
||||||
|
type="primary"
|
||||||
|
danger
|
||||||
|
:loading="backfilling"
|
||||||
|
class="mr-2"
|
||||||
|
@click="handleBackfillNameHash"
|
||||||
|
>
|
||||||
|
回填姓名MD5
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
<template #is_match="{ row }">
|
||||||
|
<Tag :color="Number(row.is_match) === 1 ? 'success' : 'error'">
|
||||||
|
{{ row.is_match_txt }}
|
||||||
|
</Tag>
|
||||||
|
</template>
|
||||||
|
<template #action="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{ label: '详情', type: 'link', size: 'small', onClick: () => openDetail(row) },
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
11
apps/web-antd/src/views/log/patient-call-log/api/index.ts
Normal file
11
apps/web-antd/src/views/log/patient-call-log/api/index.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
const prefix = 'patient-call-log/';
|
||||||
|
|
||||||
|
export async function getPatientCallLogList(data: any) {
|
||||||
|
return requestClient.get<any>(`${prefix}list`, { params: data });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPatientCallLogDetail(id: number) {
|
||||||
|
return requestClient.get<any>(`${prefix}detail`, { params: { id } });
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Descriptions } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { getPatientCallLogDetail } from '../api';
|
||||||
|
|
||||||
|
const data = ref<Record<string, any>>({});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
showConfirmButton: false,
|
||||||
|
cancelText: '关闭',
|
||||||
|
draggable: true,
|
||||||
|
onCancel() {
|
||||||
|
modalApi.close();
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen) {
|
||||||
|
if (!isOpen) {
|
||||||
|
data.value = {};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const payload = modalApi.getData<{ id?: number; values?: any }>();
|
||||||
|
const id = Number(payload?.id || payload?.values?.id || 0);
|
||||||
|
if (id <= 0) {
|
||||||
|
data.value = payload?.values || {};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.setState({ loading: true });
|
||||||
|
try {
|
||||||
|
const res = await getPatientCallLogDetail(id);
|
||||||
|
data.value = res?.data || res || {};
|
||||||
|
} catch {
|
||||||
|
data.value = payload?.values || {};
|
||||||
|
} finally {
|
||||||
|
modalApi.setState({ loading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal class="w-[90%] md:w-[640px]" title="回访记录详情">
|
||||||
|
<Descriptions bordered :column="1" size="small">
|
||||||
|
<Descriptions.Item label="ID">{{ data.id }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="就诊人ID">{{ data.up_id }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="电话">{{ data.phone }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="拨打角色">{{ data.caller_role_txt }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="门店ID">{{ data.store_id }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="结果">{{ data.result_txt }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="标签">{{ data.tags || '-' }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="结论">{{ data.conclusion || '-' }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="备注">{{ data.remark || '-' }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="拨号时间">{{ data.call_at }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="下次回访">{{ data.next_at || '-' }}</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import type { VbenFormProps } from '#/adapter/form';
|
||||||
|
|
||||||
|
import { logTimeRangeSchema } from '../../shared/normalizeLogFilters';
|
||||||
|
|
||||||
|
export const formOptions: VbenFormProps = {
|
||||||
|
collapsed: false,
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: { placeholder: '电话 / 结论 / 备注', allowClear: true },
|
||||||
|
fieldName: 'keyword',
|
||||||
|
label: '关键词',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
allowClear: true,
|
||||||
|
options: [
|
||||||
|
{ label: '医生', value: 1 },
|
||||||
|
{ label: '门店管理员', value: 2 },
|
||||||
|
{ label: '平台管理员', value: 3 },
|
||||||
|
],
|
||||||
|
placeholder: '全部',
|
||||||
|
},
|
||||||
|
fieldName: 'caller_role',
|
||||||
|
label: '拨打角色',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
allowClear: true,
|
||||||
|
options: [
|
||||||
|
{ label: '接通', value: 'connected' },
|
||||||
|
{ label: '未接', value: 'no_answer' },
|
||||||
|
{ label: '挂断', value: 'hangup' },
|
||||||
|
{ label: '空号/无效', value: 'invalid' },
|
||||||
|
],
|
||||||
|
placeholder: '全部',
|
||||||
|
},
|
||||||
|
fieldName: 'result',
|
||||||
|
label: '回访结果',
|
||||||
|
},
|
||||||
|
{ ...logTimeRangeSchema },
|
||||||
|
],
|
||||||
|
showCollapseButton: true,
|
||||||
|
submitButtonOptions: { content: '查询' },
|
||||||
|
submitOnChange: false,
|
||||||
|
submitOnEnter: true,
|
||||||
|
};
|
||||||
33
apps/web-antd/src/views/log/patient-call-log/config/table.ts
Normal file
33
apps/web-antd/src/views/log/patient-call-log/config/table.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { normalizeLogTimeFilters } from '../../shared/normalizeLogFilters';
|
||||||
|
import { getPatientCallLogList } from '../api';
|
||||||
|
|
||||||
|
export const gridOptions: VxeGridProps<any> = {
|
||||||
|
columns: [
|
||||||
|
{ field: 'id', title: 'ID', width: 80 },
|
||||||
|
{ field: 'up_id', title: '就诊人ID', width: 100 },
|
||||||
|
{ field: 'phone', title: '电话', width: 120 },
|
||||||
|
{ field: 'caller_role_txt', title: '拨打角色', width: 110 },
|
||||||
|
{ field: 'store_id', title: '门店ID', width: 90 },
|
||||||
|
{ field: 'result_txt', title: '结果', width: 90 },
|
||||||
|
{ field: 'tags', title: '标签', minWidth: 120 },
|
||||||
|
{ field: 'conclusion', title: '结论', minWidth: 140 },
|
||||||
|
{ field: 'call_at', title: '拨号时间', width: 170 },
|
||||||
|
{ title: '操作', width: 90, fixed: 'right', slots: { default: 'action' } },
|
||||||
|
],
|
||||||
|
keepSource: true,
|
||||||
|
pagerConfig: {},
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) =>
|
||||||
|
getPatientCallLogList({
|
||||||
|
page: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
...normalizeLogTimeFilters(formValues || {}),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
height: 'auto',
|
||||||
|
toolbarConfig: { search: true, refresh: true, zoom: true },
|
||||||
|
};
|
||||||
40
apps/web-antd/src/views/log/patient-call-log/index.vue
Normal file
40
apps/web-antd/src/views/log/patient-call-log/index.vue
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
/**
|
||||||
|
* 患者回访记录列表(日志审计)
|
||||||
|
*/
|
||||||
|
import { Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import { TableAction } from '#/components/table-action';
|
||||||
|
|
||||||
|
import DetailModal from './components/detail-modal.vue';
|
||||||
|
import { formOptions } from './config/search';
|
||||||
|
import { gridOptions } from './config/table';
|
||||||
|
|
||||||
|
defineOptions({ name: 'PatientCallLog' });
|
||||||
|
|
||||||
|
const [Grid] = useVbenVxeGrid({ formOptions, gridOptions });
|
||||||
|
const [DetailModalComp, detailModalApi] = useVbenModal({
|
||||||
|
connectedComponent: DetailModal,
|
||||||
|
});
|
||||||
|
|
||||||
|
function openDetail(row: any) {
|
||||||
|
detailModalApi.setData({ id: Number(row?.id || 0), values: row });
|
||||||
|
detailModalApi.open();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height title="患者回访记录">
|
||||||
|
<DetailModalComp />
|
||||||
|
<Grid>
|
||||||
|
<template #action="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{ label: '详情', type: 'link', size: 'small', onClick: () => openDetail(row) },
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
const prefix = 'product-order-price-adjust-log/';
|
||||||
|
|
||||||
|
export async function getProductOrderPriceAdjustLogList(data: any) {
|
||||||
|
return requestClient.get<any>(`${prefix}list`, { params: data });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getProductOrderPriceAdjustLogDetail(id: number) {
|
||||||
|
return requestClient.get<any>(`${prefix}detail`, { params: { id } });
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Descriptions } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { getProductOrderPriceAdjustLogDetail } from '../api';
|
||||||
|
|
||||||
|
const data = ref<Record<string, any>>({});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
showConfirmButton: false,
|
||||||
|
cancelText: '关闭',
|
||||||
|
draggable: true,
|
||||||
|
onCancel() {
|
||||||
|
modalApi.close();
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen) {
|
||||||
|
if (!isOpen) {
|
||||||
|
data.value = {};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const payload = modalApi.getData<{ id?: number; values?: any }>();
|
||||||
|
const id = Number(payload?.id || payload?.values?.id || 0);
|
||||||
|
if (id <= 0) {
|
||||||
|
data.value = payload?.values || {};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.setState({ loading: true });
|
||||||
|
try {
|
||||||
|
const res = await getProductOrderPriceAdjustLogDetail(id);
|
||||||
|
data.value = res?.data || res || {};
|
||||||
|
} catch {
|
||||||
|
data.value = payload?.values || {};
|
||||||
|
} finally {
|
||||||
|
modalApi.setState({ loading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal class="w-[90%] md:w-[640px]" title="订单调价日志详情">
|
||||||
|
<Descriptions bordered :column="2" size="small">
|
||||||
|
<Descriptions.Item label="ID">{{ data.id }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="订单ID">{{ data.product_order_id }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="药品ID">{{ data.drug_id }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="方式">{{ data.adjust_mode_txt }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="原售价">{{ data.old_price }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="新售价">{{ data.new_price }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="原供货价">{{ data.old_buy_price ?? '-' }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="新供货价">{{ data.new_buy_price ?? '-' }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="调整额">{{ data.adjust_amount }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="百分比">{{ data.percent_value ?? '-' }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="操作人">{{ data.user_id }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="挂号ID">{{ data.register_id }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="备注" :span="2">{{ data.remark || '-' }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="时间" :span="2">{{ data.created_at }}</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import type { VbenFormProps } from '#/adapter/form';
|
||||||
|
|
||||||
|
import { logTimeRangeSchema } from '../../shared/normalizeLogFilters';
|
||||||
|
|
||||||
|
export const formOptions: VbenFormProps = {
|
||||||
|
collapsed: false,
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: { placeholder: '订单ID', allowClear: true },
|
||||||
|
fieldName: 'product_order_id',
|
||||||
|
label: '订单ID',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: { placeholder: '药品ID', allowClear: true },
|
||||||
|
fieldName: 'drug_id',
|
||||||
|
label: '药品ID',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
allowClear: true,
|
||||||
|
options: [
|
||||||
|
{ label: '绝对值', value: 'absolute' },
|
||||||
|
{ label: '百分比', value: 'percent' },
|
||||||
|
],
|
||||||
|
placeholder: '全部',
|
||||||
|
},
|
||||||
|
fieldName: 'adjust_mode',
|
||||||
|
label: '调价方式',
|
||||||
|
},
|
||||||
|
{ ...logTimeRangeSchema },
|
||||||
|
],
|
||||||
|
showCollapseButton: true,
|
||||||
|
submitButtonOptions: { content: '查询' },
|
||||||
|
submitOnChange: false,
|
||||||
|
submitOnEnter: true,
|
||||||
|
};
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { normalizeLogTimeFilters } from '../../shared/normalizeLogFilters';
|
||||||
|
import { getProductOrderPriceAdjustLogList } from '../api';
|
||||||
|
|
||||||
|
export const gridOptions: VxeGridProps<any> = {
|
||||||
|
columns: [
|
||||||
|
{ field: 'id', title: 'ID', width: 70 },
|
||||||
|
{ field: 'product_order_id', title: '订单ID', width: 90 },
|
||||||
|
{ field: 'drug_id', title: '药品ID', width: 90 },
|
||||||
|
{ field: 'old_price', title: '原售价', width: 90 },
|
||||||
|
{ field: 'new_price', title: '新售价', width: 90 },
|
||||||
|
{ field: 'adjust_amount', title: '调整额', width: 90 },
|
||||||
|
{ field: 'adjust_mode_txt', title: '方式', width: 80 },
|
||||||
|
{ field: 'user_id', title: '操作人', width: 90 },
|
||||||
|
{ field: 'remark', title: '备注', minWidth: 120 },
|
||||||
|
{ field: 'created_at', title: '时间', width: 170 },
|
||||||
|
{ title: '操作', width: 90, fixed: 'right', slots: { default: 'action' } },
|
||||||
|
],
|
||||||
|
keepSource: true,
|
||||||
|
pagerConfig: {},
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) =>
|
||||||
|
getProductOrderPriceAdjustLogList({
|
||||||
|
page: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
...normalizeLogTimeFilters(formValues || {}),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
height: 'auto',
|
||||||
|
toolbarConfig: { search: true, refresh: true, zoom: true },
|
||||||
|
};
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
/**
|
||||||
|
* 订单商品价格调整日志
|
||||||
|
*/
|
||||||
|
import { Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import { TableAction } from '#/components/table-action';
|
||||||
|
|
||||||
|
import DetailModal from './components/detail-modal.vue';
|
||||||
|
import { formOptions } from './config/search';
|
||||||
|
import { gridOptions } from './config/table';
|
||||||
|
|
||||||
|
defineOptions({ name: 'ProductOrderPriceAdjustLog' });
|
||||||
|
|
||||||
|
const [Grid] = useVbenVxeGrid({ formOptions, gridOptions });
|
||||||
|
const [DetailModalComp, detailModalApi] = useVbenModal({
|
||||||
|
connectedComponent: DetailModal,
|
||||||
|
});
|
||||||
|
|
||||||
|
function openDetail(row: any) {
|
||||||
|
detailModalApi.setData({ id: Number(row?.id || 0), values: row });
|
||||||
|
detailModalApi.open();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height title="订单调价日志">
|
||||||
|
<DetailModalComp />
|
||||||
|
<Grid>
|
||||||
|
<template #action="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{ label: '详情', type: 'link', size: 'small', onClick: () => openDetail(row) },
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
30
apps/web-antd/src/views/log/shared/normalizeLogFilters.ts
Normal file
30
apps/web-antd/src/views/log/shared/normalizeLogFilters.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* 日志列表通用:search_time → start_time/end_time
|
||||||
|
*/
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
export function normalizeLogTimeFilters(formValues: Record<string, any> = {}) {
|
||||||
|
const params: Record<string, any> = { ...formValues };
|
||||||
|
const range = params.search_time;
|
||||||
|
delete params.search_time;
|
||||||
|
if (Array.isArray(range) && range.length === 2 && range[0] && range[1]) {
|
||||||
|
params.start_time = dayjs(range[0]).startOf('day').unix();
|
||||||
|
params.end_time = dayjs(range[1]).endOf('day').unix();
|
||||||
|
}
|
||||||
|
Object.keys(params).forEach((k) => {
|
||||||
|
if (params[k] === '' || params[k] === undefined || params[k] === null) {
|
||||||
|
delete params[k];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const logTimeRangeSchema = {
|
||||||
|
component: 'RangePicker' as const,
|
||||||
|
componentProps: {
|
||||||
|
format: 'YYYY-MM-DD',
|
||||||
|
valueFormat: 'YYYY-MM-DD',
|
||||||
|
},
|
||||||
|
fieldName: 'search_time',
|
||||||
|
label: '时间范围',
|
||||||
|
};
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
const prefix = 'store-drug-update-log/';
|
||||||
|
|
||||||
|
export async function getStoreDrugUpdateLogList(data: any) {
|
||||||
|
return requestClient.get<any>(`${prefix}list`, { params: data });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStoreDrugUpdateLogDetail(id: number) {
|
||||||
|
return requestClient.get<any>(`${prefix}detail`, { params: { id } });
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Descriptions } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { getStoreDrugUpdateLogDetail } from '../api';
|
||||||
|
|
||||||
|
const data = ref<Record<string, any>>({});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
showConfirmButton: false,
|
||||||
|
cancelText: '关闭',
|
||||||
|
draggable: true,
|
||||||
|
onCancel() {
|
||||||
|
modalApi.close();
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen) {
|
||||||
|
if (!isOpen) {
|
||||||
|
data.value = {};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const payload = modalApi.getData<{ id?: number; values?: any }>();
|
||||||
|
const id = Number(payload?.id || payload?.values?.id || 0);
|
||||||
|
if (id <= 0) {
|
||||||
|
data.value = payload?.values || {};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.setState({ loading: true });
|
||||||
|
try {
|
||||||
|
const res = await getStoreDrugUpdateLogDetail(id);
|
||||||
|
data.value = res?.data || res || {};
|
||||||
|
} catch {
|
||||||
|
data.value = payload?.values || {};
|
||||||
|
} finally {
|
||||||
|
modalApi.setState({ loading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal class="w-[90%] md:w-[640px]" title="仓库价格修改日志详情">
|
||||||
|
<Descriptions bordered :column="2" size="small">
|
||||||
|
<Descriptions.Item label="ID">{{ data.id }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="类型">{{ data.type_txt }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="操作人">{{ data.operator_name || data.user_id }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="药品">{{ data.drug_name || data.drug_id }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="编码">{{ data.drug_number || '-' }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="门店ID">{{ data.store_id }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="原供货价">{{ data.old_buy }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="新供货价">{{ data.new_buy }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="原售价">{{ data.old_price }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="新售价">{{ data.new_price }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="IP" :span="2">{{ data.ip }} {{ data.ip_address }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="时间" :span="2">{{ data.created_at }}</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import type { VbenFormProps } from '#/adapter/form';
|
||||||
|
|
||||||
|
import { logTimeRangeSchema } from '../../shared/normalizeLogFilters';
|
||||||
|
|
||||||
|
export const formOptions: VbenFormProps = {
|
||||||
|
collapsed: false,
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
allowClear: true,
|
||||||
|
options: [
|
||||||
|
{ label: '总仓库', value: 0 },
|
||||||
|
{ label: '诊所|药店', value: 1 },
|
||||||
|
],
|
||||||
|
placeholder: '全部',
|
||||||
|
},
|
||||||
|
fieldName: 'type',
|
||||||
|
label: '仓库类型',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: { placeholder: '药品ID', allowClear: true },
|
||||||
|
fieldName: 'drug_id',
|
||||||
|
label: '药品ID',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: { placeholder: '门店ID', allowClear: true },
|
||||||
|
fieldName: 'store_id',
|
||||||
|
label: '门店ID',
|
||||||
|
},
|
||||||
|
{ ...logTimeRangeSchema },
|
||||||
|
],
|
||||||
|
showCollapseButton: true,
|
||||||
|
submitButtonOptions: { content: '查询' },
|
||||||
|
submitOnChange: false,
|
||||||
|
submitOnEnter: true,
|
||||||
|
};
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { normalizeLogTimeFilters } from '../../shared/normalizeLogFilters';
|
||||||
|
import { getStoreDrugUpdateLogList } from '../api';
|
||||||
|
|
||||||
|
export const gridOptions: VxeGridProps<any> = {
|
||||||
|
columns: [
|
||||||
|
{ field: 'id', title: 'ID', width: 70 },
|
||||||
|
{ field: 'operator_name', title: '操作人', width: 100 },
|
||||||
|
{ field: 'type_txt', title: '类型', width: 100 },
|
||||||
|
{ field: 'drug_name', title: '药品', minWidth: 120 },
|
||||||
|
{ field: 'drug_number', title: '编码', width: 110 },
|
||||||
|
{ field: 'old_buy', title: '原供货价', width: 90 },
|
||||||
|
{ field: 'new_buy', title: '新供货价', width: 90 },
|
||||||
|
{ field: 'old_price', title: '原售价', width: 90 },
|
||||||
|
{ field: 'new_price', title: '新售价', width: 90 },
|
||||||
|
{ field: 'store_id', title: '门店ID', width: 80 },
|
||||||
|
{ field: 'created_at', title: '时间', width: 170 },
|
||||||
|
{ title: '操作', width: 90, fixed: 'right', slots: { default: 'action' } },
|
||||||
|
],
|
||||||
|
keepSource: true,
|
||||||
|
pagerConfig: {},
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) =>
|
||||||
|
getStoreDrugUpdateLogList({
|
||||||
|
page: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
...normalizeLogTimeFilters(formValues || {}),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
height: 'auto',
|
||||||
|
toolbarConfig: { search: true, refresh: true, zoom: true },
|
||||||
|
};
|
||||||
40
apps/web-antd/src/views/log/store-drug-update-log/index.vue
Normal file
40
apps/web-antd/src/views/log/store-drug-update-log/index.vue
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
/**
|
||||||
|
* 仓库价格修改日志(旧表 xk_store_drug_update_log)
|
||||||
|
*/
|
||||||
|
import { Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import { TableAction } from '#/components/table-action';
|
||||||
|
|
||||||
|
import DetailModal from './components/detail-modal.vue';
|
||||||
|
import { formOptions } from './config/search';
|
||||||
|
import { gridOptions } from './config/table';
|
||||||
|
|
||||||
|
defineOptions({ name: 'StoreDrugUpdateLog' });
|
||||||
|
|
||||||
|
const [Grid] = useVbenVxeGrid({ formOptions, gridOptions });
|
||||||
|
const [DetailModalComp, detailModalApi] = useVbenModal({
|
||||||
|
connectedComponent: DetailModal,
|
||||||
|
});
|
||||||
|
|
||||||
|
function openDetail(row: any) {
|
||||||
|
detailModalApi.setData({ id: Number(row?.id || 0), values: row });
|
||||||
|
detailModalApi.open();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height title="仓库价格修改日志">
|
||||||
|
<DetailModalComp />
|
||||||
|
<Grid>
|
||||||
|
<template #action="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{ label: '详情', type: 'link', size: 'small', onClick: () => openDetail(row) },
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
const prefix = 'sync-china-order-log/';
|
||||||
|
|
||||||
|
export async function getSyncChinaOrderLogList(data: any) {
|
||||||
|
return requestClient.get<any>(`${prefix}list`, { params: data });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSyncChinaOrderLogDetail(id: number) {
|
||||||
|
return requestClient.get<any>(`${prefix}detail`, { params: { id } });
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Descriptions, Tag } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { getSyncChinaOrderLogDetail } from '../api';
|
||||||
|
|
||||||
|
const data = ref<Record<string, any>>({});
|
||||||
|
const contentText = computed(() => {
|
||||||
|
const c = data.value.content;
|
||||||
|
if (c == null) return '(无)';
|
||||||
|
try {
|
||||||
|
return typeof c === 'string' ? c : JSON.stringify(c, null, 2);
|
||||||
|
} catch {
|
||||||
|
return String(c);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
showConfirmButton: false,
|
||||||
|
cancelText: '关闭',
|
||||||
|
draggable: true,
|
||||||
|
onCancel() {
|
||||||
|
modalApi.close();
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen) {
|
||||||
|
if (!isOpen) {
|
||||||
|
data.value = {};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const payload = modalApi.getData<{ id?: number; values?: any }>();
|
||||||
|
const id = Number(payload?.id || payload?.values?.id || 0);
|
||||||
|
if (id <= 0) {
|
||||||
|
data.value = payload?.values || {};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.setState({ loading: true });
|
||||||
|
try {
|
||||||
|
const res = await getSyncChinaOrderLogDetail(id);
|
||||||
|
data.value = res?.data || res || {};
|
||||||
|
} catch {
|
||||||
|
data.value = payload?.values || {};
|
||||||
|
} finally {
|
||||||
|
modalApi.setState({ loading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal class="w-[90%] md:w-[720px]" title="同步中药 ERP 日志详情">
|
||||||
|
<Descriptions bordered :column="1" size="small">
|
||||||
|
<Descriptions.Item label="ID">{{ data.id }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="订单ID">{{ data.order_id }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="结果">
|
||||||
|
<Tag :color="Number(data.is_success) === 0 ? 'success' : 'error'">
|
||||||
|
{{ data.is_success_txt }}
|
||||||
|
</Tag>
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="时间">{{ data.created_at }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="内容">
|
||||||
|
<pre class="max-h-80 overflow-auto whitespace-pre-wrap break-all text-xs">{{ contentText }}</pre>
|
||||||
|
</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import type { VbenFormProps } from '#/adapter/form';
|
||||||
|
|
||||||
|
export const formOptions: VbenFormProps = {
|
||||||
|
collapsed: false,
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: { placeholder: '订单ID', allowClear: true },
|
||||||
|
fieldName: 'order_id',
|
||||||
|
label: '订单ID',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
allowClear: true,
|
||||||
|
options: [
|
||||||
|
{ label: '成功', value: 0 },
|
||||||
|
{ label: '失败', value: 1 },
|
||||||
|
],
|
||||||
|
placeholder: '全部',
|
||||||
|
},
|
||||||
|
fieldName: 'is_success',
|
||||||
|
label: '结果',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: { placeholder: '内容关键词', allowClear: true },
|
||||||
|
fieldName: 'keyword',
|
||||||
|
label: '关键词',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
showCollapseButton: true,
|
||||||
|
submitButtonOptions: { content: '查询' },
|
||||||
|
submitOnChange: false,
|
||||||
|
submitOnEnter: true,
|
||||||
|
};
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { getSyncChinaOrderLogList } from '../api';
|
||||||
|
|
||||||
|
export const gridOptions: VxeGridProps<any> = {
|
||||||
|
columns: [
|
||||||
|
{ field: 'id', title: 'ID', width: 80 },
|
||||||
|
{ field: 'order_id', title: '订单ID', width: 100 },
|
||||||
|
{
|
||||||
|
field: 'is_success_txt',
|
||||||
|
title: '结果',
|
||||||
|
width: 90,
|
||||||
|
slots: { default: 'is_success' },
|
||||||
|
},
|
||||||
|
{ field: 'content_preview', title: '内容预览', minWidth: 260 },
|
||||||
|
{ field: 'created_at', title: '同步时间', width: 180 },
|
||||||
|
{ title: '操作', width: 90, fixed: 'right', slots: { default: 'action' } },
|
||||||
|
],
|
||||||
|
keepSource: true,
|
||||||
|
pagerConfig: {},
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) =>
|
||||||
|
getSyncChinaOrderLogList({
|
||||||
|
page: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
...formValues,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
height: 'auto',
|
||||||
|
toolbarConfig: { search: true, refresh: true, zoom: true },
|
||||||
|
};
|
||||||
47
apps/web-antd/src/views/log/sync-china-order-log/index.vue
Normal file
47
apps/web-antd/src/views/log/sync-china-order-log/index.vue
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
/**
|
||||||
|
* 同步中药 ERP 日志
|
||||||
|
*/
|
||||||
|
import { Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Tag } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import { TableAction } from '#/components/table-action';
|
||||||
|
|
||||||
|
import DetailModal from './components/detail-modal.vue';
|
||||||
|
import { formOptions } from './config/search';
|
||||||
|
import { gridOptions } from './config/table';
|
||||||
|
|
||||||
|
defineOptions({ name: 'SyncChinaOrderLog' });
|
||||||
|
|
||||||
|
const [Grid] = useVbenVxeGrid({ formOptions, gridOptions });
|
||||||
|
const [DetailModalComp, detailModalApi] = useVbenModal({
|
||||||
|
connectedComponent: DetailModal,
|
||||||
|
});
|
||||||
|
|
||||||
|
function openDetail(row: any) {
|
||||||
|
detailModalApi.setData({ id: Number(row?.id || 0), values: row });
|
||||||
|
detailModalApi.open();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height title="同步中药 ERP 日志">
|
||||||
|
<DetailModalComp />
|
||||||
|
<Grid>
|
||||||
|
<template #is_success="{ row }">
|
||||||
|
<Tag :color="Number(row.is_success) === 0 ? 'success' : 'error'">
|
||||||
|
{{ row.is_success_txt }}
|
||||||
|
</Tag>
|
||||||
|
</template>
|
||||||
|
<template #action="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{ label: '详情', type: 'link', size: 'small', onClick: () => openDetail(row) },
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* 仓库药品修改记录 API
|
||||||
|
*/
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
const prefix = 'warehouse-drug-change-log/';
|
||||||
|
|
||||||
|
export async function getWarehouseDrugChangeLogList(data: any) {
|
||||||
|
return requestClient.get<any>(`${prefix}list`, { params: data });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getWarehouseDrugChangeLogDetail(id: number) {
|
||||||
|
return requestClient.get<any>(`${prefix}detail`, { params: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getWarehouseDrugChangeTypeOption() {
|
||||||
|
return requestClient.get<{ value: number; label: string }[]>(
|
||||||
|
`${prefix}change-type-option`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getWarehouseDrugUserTypeOption() {
|
||||||
|
return requestClient.get<{ value: number; label: string }[]>(
|
||||||
|
`${prefix}user-type-option`,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
/**
|
||||||
|
* 仓库修改记录详情:展示操作前后药品快照 JSON
|
||||||
|
*/
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Descriptions, Tag } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { getWarehouseDrugChangeLogDetail } from '../api';
|
||||||
|
|
||||||
|
const data = ref<Record<string, any>>({});
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
function formatSnapshot(snap: unknown): string {
|
||||||
|
if (snap == null || snap === '') {
|
||||||
|
return '(无)';
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.stringify(snap, null, 2);
|
||||||
|
} catch {
|
||||||
|
return String(snap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
fullscreenButton: true,
|
||||||
|
draggable: true,
|
||||||
|
showConfirmButton: false,
|
||||||
|
cancelText: '关闭',
|
||||||
|
onCancel() {
|
||||||
|
modalApi.close();
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
data.value = {};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const payload = modalApi.getData<{ id?: number; values?: Record<string, any> }>();
|
||||||
|
const id = Number(payload?.id || payload?.values?.id || 0);
|
||||||
|
if (id <= 0) {
|
||||||
|
data.value = payload?.values || {};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loading.value = true;
|
||||||
|
modalApi.setState({ loading: true });
|
||||||
|
try {
|
||||||
|
const res = await getWarehouseDrugChangeLogDetail(id);
|
||||||
|
data.value = res?.data || res || {};
|
||||||
|
} catch {
|
||||||
|
data.value = payload?.values || {};
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
modalApi.setState({ loading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal class="w-[90%] md:w-[720px]" title="仓库修改记录详情">
|
||||||
|
<Descriptions
|
||||||
|
:column="{ xxl: 2, xl: 2, lg: 2, md: 1, sm: 1, xs: 1 }"
|
||||||
|
bordered
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<Descriptions.Item label="ID">{{ data.id }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="操作时间">{{ data.created_at }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="操作人">
|
||||||
|
{{ data.operator_name || '-' }}(ID: {{ data.operator_id || 0 }})
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="用户类型">
|
||||||
|
<Tag>{{ data.user_type_txt || data.user_type }}</Tag>
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="变更类型">
|
||||||
|
<Tag color="blue">{{ data.change_type_txt || data.change_type }}</Tag>
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="门店ID">{{ data.store_id || 0 }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="药品名称">{{ data.drug_name || '-' }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="药品编码">{{ data.drug_number || '-' }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="药品ID">{{ data.drug_id || 0 }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="仓库药品ID">{{ data.store_drug_id || 0 }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="批次号" :span="2">{{ data.batch_no || '-' }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="备注" :span="2">{{ data.remark || '-' }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="IP" :span="2">{{ data.ip || '-' }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="操作前快照" :span="2">
|
||||||
|
<pre class="snap-pre">{{ formatSnapshot(data.before_snapshot) }}</pre>
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="操作后快照" :span="2">
|
||||||
|
<pre class="snap-pre">{{ formatSnapshot(data.after_snapshot) }}</pre>
|
||||||
|
</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.snap-pre {
|
||||||
|
margin: 0;
|
||||||
|
max-height: 280px;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: hsl(var(--muted) / 0.4);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* 仓库修改记录筛选项常量(兜底;优先用接口 option)
|
||||||
|
*/
|
||||||
|
export const CHANGE_TYPE_OPTIONS = [
|
||||||
|
{ label: '单个', value: 1 },
|
||||||
|
{ label: '批量', value: 2 },
|
||||||
|
{ label: '同步到门店', value: 3 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const USER_TYPE_OPTIONS = [
|
||||||
|
{ label: '门店', value: 1 },
|
||||||
|
{ label: '平台', value: 2 },
|
||||||
|
];
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import type { VbenFormProps } from '#/adapter/form';
|
||||||
|
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
import { CHANGE_TYPE_OPTIONS, USER_TYPE_OPTIONS } from './constants';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仓库修改记录搜索表单
|
||||||
|
*/
|
||||||
|
export const formOptions: VbenFormProps = {
|
||||||
|
collapsed: false,
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '药名 / 编码 / 操作人 / 批次号',
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
fieldName: 'keyword',
|
||||||
|
label: '关键词',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
allowClear: true,
|
||||||
|
options: [...CHANGE_TYPE_OPTIONS],
|
||||||
|
placeholder: '全部',
|
||||||
|
},
|
||||||
|
fieldName: 'change_type',
|
||||||
|
label: '变更类型',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
allowClear: true,
|
||||||
|
options: [...USER_TYPE_OPTIONS],
|
||||||
|
placeholder: '全部',
|
||||||
|
},
|
||||||
|
fieldName: 'user_type',
|
||||||
|
label: '用户类型',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'RangePicker',
|
||||||
|
componentProps: {
|
||||||
|
format: 'YYYY-MM-DD',
|
||||||
|
valueFormat: 'YYYY-MM-DD',
|
||||||
|
},
|
||||||
|
fieldName: 'search_time',
|
||||||
|
label: '时间范围',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
showCollapseButton: true,
|
||||||
|
submitButtonOptions: {
|
||||||
|
content: '查询',
|
||||||
|
},
|
||||||
|
submitOnChange: false,
|
||||||
|
submitOnEnter: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* search_time → start_time/end_time 时间戳
|
||||||
|
*/
|
||||||
|
export function normalizeChangeLogFilters(formValues: Record<string, any> = {}) {
|
||||||
|
const params: Record<string, any> = { ...formValues };
|
||||||
|
const range = params.search_time;
|
||||||
|
delete params.search_time;
|
||||||
|
if (Array.isArray(range) && range.length === 2 && range[0] && range[1]) {
|
||||||
|
params.start_time = dayjs(range[0]).startOf('day').unix();
|
||||||
|
params.end_time = dayjs(range[1]).endOf('day').unix();
|
||||||
|
}
|
||||||
|
Object.keys(params).forEach((k) => {
|
||||||
|
if (params[k] === '' || params[k] === undefined || params[k] === null) {
|
||||||
|
delete params[k];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return params;
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { getWarehouseDrugChangeLogList } from '../api';
|
||||||
|
import { normalizeChangeLogFilters } from './search';
|
||||||
|
|
||||||
|
export const gridOptions: VxeGridProps<any> = {
|
||||||
|
checkboxConfig: {
|
||||||
|
highlight: true,
|
||||||
|
labelField: '',
|
||||||
|
},
|
||||||
|
columns: [
|
||||||
|
{ field: 'id', title: 'ID', width: 80 },
|
||||||
|
{ field: 'operator_name', title: '操作人', minWidth: 100 },
|
||||||
|
{
|
||||||
|
field: 'user_type_txt',
|
||||||
|
title: '用户类型',
|
||||||
|
width: 90,
|
||||||
|
slots: { default: 'user_type' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'change_type_txt',
|
||||||
|
title: '变更类型',
|
||||||
|
width: 110,
|
||||||
|
slots: { default: 'change_type' },
|
||||||
|
},
|
||||||
|
{ field: 'drug_name', title: '药品名称', minWidth: 120 },
|
||||||
|
{ field: 'drug_number', title: '药品编码', minWidth: 110 },
|
||||||
|
{ field: 'store_id', title: '门店ID', width: 90 },
|
||||||
|
{ field: 'batch_no', title: '批次号', minWidth: 140 },
|
||||||
|
{ field: 'remark', title: '备注', minWidth: 140 },
|
||||||
|
{ field: 'created_at', title: '操作时间', width: 170 },
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 90,
|
||||||
|
fixed: 'right',
|
||||||
|
slots: { default: 'action' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
keepSource: true,
|
||||||
|
pagerConfig: {},
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
return await getWarehouseDrugChangeLogList({
|
||||||
|
page: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
...normalizeChangeLogFilters(formValues || {}),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
height: 'auto',
|
||||||
|
toolbarConfig: {
|
||||||
|
search: true,
|
||||||
|
refresh: true,
|
||||||
|
zoom: true,
|
||||||
|
slots: {
|
||||||
|
buttons: 'toolbar-buttons',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
108
apps/web-antd/src/views/log/warehouse-drug-change-log/index.vue
Normal file
108
apps/web-antd/src/views/log/warehouse-drug-change-log/index.vue
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
/**
|
||||||
|
* 仓库药品修改记录列表(日志审计)
|
||||||
|
* 查看单个/批量/同步到门店的前后快照
|
||||||
|
*/
|
||||||
|
import { onMounted } from 'vue';
|
||||||
|
|
||||||
|
import { Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Tag } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import { TableAction } from '#/components/table-action';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getWarehouseDrugChangeTypeOption,
|
||||||
|
getWarehouseDrugUserTypeOption,
|
||||||
|
} from './api';
|
||||||
|
import DetailModal from './components/detail-modal.vue';
|
||||||
|
import { formOptions } from './config/search';
|
||||||
|
import { gridOptions } from './config/table';
|
||||||
|
|
||||||
|
defineOptions({ name: 'WarehouseDrugChangeLog' });
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions,
|
||||||
|
gridOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [DetailModalComp, detailModalApi] = useVbenModal({
|
||||||
|
connectedComponent: DetailModal,
|
||||||
|
});
|
||||||
|
|
||||||
|
function openDetail(row: Record<string, any>) {
|
||||||
|
detailModalApi.setData({ id: Number(row?.id || 0), values: row });
|
||||||
|
detailModalApi.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeTypeColor(type: number) {
|
||||||
|
if (type === 2) return 'orange';
|
||||||
|
if (type === 3) return 'purple';
|
||||||
|
return 'blue';
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
const [changeOpts, userOpts] = await Promise.all([
|
||||||
|
getWarehouseDrugChangeTypeOption(),
|
||||||
|
getWarehouseDrugUserTypeOption(),
|
||||||
|
]);
|
||||||
|
const changeOptions = Array.isArray(changeOpts)
|
||||||
|
? changeOpts
|
||||||
|
: changeOpts?.data || [];
|
||||||
|
const userOptions = Array.isArray(userOpts) ? userOpts : userOpts?.data || [];
|
||||||
|
gridApi.formApi?.updateSchema?.([
|
||||||
|
{
|
||||||
|
fieldName: 'change_type',
|
||||||
|
componentProps: {
|
||||||
|
allowClear: true,
|
||||||
|
options: changeOptions,
|
||||||
|
placeholder: '全部',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'user_type',
|
||||||
|
componentProps: {
|
||||||
|
allowClear: true,
|
||||||
|
options: userOptions,
|
||||||
|
placeholder: '全部',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
} catch {
|
||||||
|
// 接口失败时沿用 constants 兜底
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height title="仓库修改记录">
|
||||||
|
<DetailModalComp />
|
||||||
|
<Grid>
|
||||||
|
<template #toolbar-buttons />
|
||||||
|
<template #user_type="{ row }">
|
||||||
|
<Tag :color="Number(row.user_type) === 2 ? 'geekblue' : 'green'">
|
||||||
|
{{ row.user_type_txt || row.user_type }}
|
||||||
|
</Tag>
|
||||||
|
</template>
|
||||||
|
<template #change_type="{ row }">
|
||||||
|
<Tag :color="changeTypeColor(Number(row.change_type))">
|
||||||
|
{{ row.change_type_txt || row.change_type }}
|
||||||
|
</Tag>
|
||||||
|
</template>
|
||||||
|
<template #action="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: '详情',
|
||||||
|
type: 'link',
|
||||||
|
size: 'small',
|
||||||
|
onClick: () => openDetail(row),
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
@@ -87,7 +87,7 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
: null;
|
: null;
|
||||||
lockPlatformId.value = isOpen ? !!openData.lockPlatformId : false;
|
lockPlatformId.value = isOpen ? !!openData.lockPlatformId : false;
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
formApi.resetFields();
|
formApi.resetForm();
|
||||||
selectedPlatformId.value = undefined;
|
selectedPlatformId.value = undefined;
|
||||||
platforms.value = [];
|
platforms.value = [];
|
||||||
return;
|
return;
|
||||||
@@ -119,7 +119,7 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
formApi.setValues({ ...values, platform_id: pid, api_key: '' });
|
formApi.setValues({ ...values, platform_id: pid, api_key: '' });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
formApi.resetFields();
|
formApi.resetForm();
|
||||||
const presetPid = Number(openData.platform_id || values?.platform_id || 0) || undefined;
|
const presetPid = Number(openData.platform_id || values?.platform_id || 0) || undefined;
|
||||||
const firstId = presetPid || platforms.value[0]?.id;
|
const firstId = presetPid || platforms.value[0]?.id;
|
||||||
selectedPlatformId.value = firstId;
|
selectedPlatformId.value = firstId;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { AI_GENERATION_SCENE_OPTIONS, AI_GENERATION_STATUS_OPTIONS } from './con
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* AI 生成记录搜索表单
|
* AI 生成记录搜索表单
|
||||||
* 时间范围字段 search_time,查询前映射为 start_time/end_time 时间戳
|
* 平台/模型用下拉(options 在页面 onMounted 注入),时间范围映射为 start_time/end_time
|
||||||
*/
|
*/
|
||||||
export const formOptions: VbenFormProps = {
|
export const formOptions: VbenFormProps = {
|
||||||
collapsed: false,
|
collapsed: false,
|
||||||
@@ -14,7 +14,8 @@ export const formOptions: VbenFormProps = {
|
|||||||
{
|
{
|
||||||
component: 'VbenInput',
|
component: 'VbenInput',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
placeholder: '名称 / 模型 / 错误摘要',
|
placeholder: '名称 / 错误摘要',
|
||||||
|
allowClear: true,
|
||||||
},
|
},
|
||||||
defaultValue: '',
|
defaultValue: '',
|
||||||
fieldName: 'keyword',
|
fieldName: 'keyword',
|
||||||
@@ -43,32 +44,31 @@ export const formOptions: VbenFormProps = {
|
|||||||
label: '场景',
|
label: '场景',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: 'VbenInput',
|
component: 'Select',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
placeholder: '如 spark / deepseek',
|
placeholder: '全部平台',
|
||||||
|
allowClear: true,
|
||||||
|
showSearch: true,
|
||||||
|
optionFilterProp: 'label',
|
||||||
|
options: [],
|
||||||
},
|
},
|
||||||
defaultValue: '',
|
defaultValue: undefined,
|
||||||
fieldName: 'provider',
|
fieldName: 'platform_id',
|
||||||
label: '供应商',
|
label: '平台',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: 'VbenInput',
|
component: 'Select',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
placeholder: '模型编码',
|
placeholder: '全部模型',
|
||||||
|
allowClear: true,
|
||||||
|
showSearch: true,
|
||||||
|
optionFilterProp: 'label',
|
||||||
|
options: [],
|
||||||
},
|
},
|
||||||
defaultValue: '',
|
defaultValue: undefined,
|
||||||
fieldName: 'model',
|
fieldName: 'model',
|
||||||
label: '模型',
|
label: '模型',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
component: 'VbenInput',
|
|
||||||
componentProps: {
|
|
||||||
placeholder: '密钥 ID',
|
|
||||||
},
|
|
||||||
defaultValue: '',
|
|
||||||
fieldName: 'api_key_id',
|
|
||||||
label: '密钥ID',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
component: 'RangePicker',
|
component: 'RangePicker',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
/**
|
/**
|
||||||
* AI 生成记录列表页
|
* AI 生成记录列表页
|
||||||
* 顶部用量摘要(随当前筛选刷新)+ VxeGrid 明细
|
* 顶部用量摘要(随当前筛选刷新)+ VxeGrid 明细
|
||||||
|
* 平台/模型筛选下拉:从 option 接口注入,选平台后联动刷新模型列表
|
||||||
*/
|
*/
|
||||||
import { onMounted, reactive } from 'vue';
|
import { onMounted, reactive, ref } from 'vue';
|
||||||
|
|
||||||
import { Page, useVbenModal } from '@vben/common-ui';
|
import { Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
@@ -12,6 +13,8 @@ import { Card, Col, Row, Tag } from 'ant-design-vue';
|
|||||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
import { TableAction } from '#/components/table-action';
|
import { TableAction } from '#/components/table-action';
|
||||||
|
|
||||||
|
import { getAiModelOption } from '../platform/api/model';
|
||||||
|
import { getAiPlatformOption } from '../platform/api';
|
||||||
import { getAiGenerationList, getAiGenerationUsageStats } from './api';
|
import { getAiGenerationList, getAiGenerationUsageStats } from './api';
|
||||||
import DetailModal from './components/detail-modal.vue';
|
import DetailModal from './components/detail-modal.vue';
|
||||||
import { formOptions, normalizeAiGenerationFilters } from './config/search';
|
import { formOptions, normalizeAiGenerationFilters } from './config/search';
|
||||||
@@ -31,6 +34,9 @@ const stats = reactive({
|
|||||||
fail_count: 0,
|
fail_count: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** 全量模型 option,选平台时按 platform_id 过滤 */
|
||||||
|
const allModelOptions = ref<Array<{ label: string; value: string; platform_id?: number }>>([]);
|
||||||
|
|
||||||
/** 把毫秒格式化为秒文案 */
|
/** 把毫秒格式化为秒文案 */
|
||||||
function formatDuration(ms: number) {
|
function formatDuration(ms: number) {
|
||||||
return formatDurationMs(ms);
|
return formatDurationMs(ms);
|
||||||
@@ -55,8 +61,45 @@ async function refreshStats(formValues?: Record<string, any>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const [Grid] = useVbenVxeGrid({
|
/** 按平台刷新模型下拉;清空平台时恢复全部模型 */
|
||||||
formOptions,
|
function refreshModelSchema(platformId?: number) {
|
||||||
|
const filtered =
|
||||||
|
platformId && platformId > 0
|
||||||
|
? allModelOptions.value.filter((m) => Number(m.platform_id) === platformId)
|
||||||
|
: allModelOptions.value;
|
||||||
|
gridApi.formApi?.updateSchema?.([
|
||||||
|
{
|
||||||
|
fieldName: 'model',
|
||||||
|
componentProps: {
|
||||||
|
allowClear: true,
|
||||||
|
showSearch: true,
|
||||||
|
optionFilterProp: 'label',
|
||||||
|
options: filtered,
|
||||||
|
placeholder: '全部模型',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions: {
|
||||||
|
...formOptions,
|
||||||
|
schema: formOptions.schema?.map((item) =>
|
||||||
|
item.fieldName === 'platform_id'
|
||||||
|
? {
|
||||||
|
...item,
|
||||||
|
componentProps: {
|
||||||
|
...item.componentProps,
|
||||||
|
onChange: (val: number | undefined) => {
|
||||||
|
// 切换平台时清掉已选模型,避免跨平台残留
|
||||||
|
gridApi.formApi?.setFieldValue?.('model', undefined);
|
||||||
|
refreshModelSchema(val ? Number(val) : undefined);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: item,
|
||||||
|
),
|
||||||
|
},
|
||||||
gridOptions: {
|
gridOptions: {
|
||||||
...gridOptions,
|
...gridOptions,
|
||||||
proxyConfig: {
|
proxyConfig: {
|
||||||
@@ -90,8 +133,42 @@ function statusColor(status: number) {
|
|||||||
return 'processing';
|
return 'processing';
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
void refreshStats();
|
void refreshStats();
|
||||||
|
try {
|
||||||
|
const [platformRes, modelRes] = await Promise.all([
|
||||||
|
getAiPlatformOption(),
|
||||||
|
getAiModelOption(),
|
||||||
|
]);
|
||||||
|
const platforms = Array.isArray(platformRes)
|
||||||
|
? platformRes
|
||||||
|
: platformRes?.data || [];
|
||||||
|
const models = Array.isArray(modelRes) ? modelRes : modelRes?.data || [];
|
||||||
|
allModelOptions.value = models.map((m: any) => ({
|
||||||
|
label: m.label || `${m.name || ''}(${m.code || m.value || ''})`,
|
||||||
|
value: String(m.value ?? m.code ?? ''),
|
||||||
|
platform_id: Number(m.platform_id || 0),
|
||||||
|
}));
|
||||||
|
gridApi.formApi?.updateSchema?.([
|
||||||
|
{
|
||||||
|
fieldName: 'platform_id',
|
||||||
|
componentProps: {
|
||||||
|
allowClear: true,
|
||||||
|
showSearch: true,
|
||||||
|
optionFilterProp: 'label',
|
||||||
|
options: platforms,
|
||||||
|
placeholder: '全部平台',
|
||||||
|
onChange: (val: number | undefined) => {
|
||||||
|
gridApi.formApi?.setFieldValue?.('model', undefined);
|
||||||
|
refreshModelSchema(val ? Number(val) : undefined);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
refreshModelSchema();
|
||||||
|
} catch {
|
||||||
|
// option 失败时下拉为空,仍可用关键词筛
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
? modalApi.getData()?.onSuccess || null
|
? modalApi.getData()?.onSuccess || null
|
||||||
: null;
|
: null;
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
formApi.resetFields();
|
formApi.resetForm();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { values, update } = modalApi.getData<Record<string, any>>() || {};
|
const { values, update } = modalApi.getData<Record<string, any>>() || {};
|
||||||
@@ -53,7 +53,7 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
if (values && update) {
|
if (values && update) {
|
||||||
formApi.setValues({ ...values });
|
formApi.setValues({ ...values });
|
||||||
} else {
|
} else {
|
||||||
formApi.resetFields();
|
formApi.resetForm();
|
||||||
formApi.setValues({ sort: 0, status: 1 });
|
formApi.setValues({ sort: 0, status: 1 });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
},
|
},
|
||||||
onOpenChange(isOpen: boolean) {
|
onOpenChange(isOpen: boolean) {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
formApi.resetFields();
|
formApi.resetForm();
|
||||||
onSuccess.value = null;
|
onSuccess.value = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -125,7 +125,7 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
if (data.values && data.update) {
|
if (data.values && data.update) {
|
||||||
formApi.setValues({ ...data.values });
|
formApi.setValues({ ...data.values });
|
||||||
} else {
|
} else {
|
||||||
formApi.resetFields();
|
formApi.resetForm();
|
||||||
formApi.setValues({
|
formApi.setValues({
|
||||||
platform_id: data.platform_id || data.values?.platform_id || 0,
|
platform_id: data.platform_id || data.values?.platform_id || 0,
|
||||||
sort: 0,
|
sort: 0,
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ const [FormModal, formModalApi] = useVbenModal({
|
|||||||
},
|
},
|
||||||
onOpenChange(isOpen: boolean) {
|
onOpenChange(isOpen: boolean) {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
formApi.resetFields();
|
formApi.resetForm();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const data = formModalApi.getData<{ values?: any; update?: boolean }>();
|
const data = formModalApi.getData<{ values?: any; update?: boolean }>();
|
||||||
@@ -185,7 +185,7 @@ const [FormModal, formModalApi] = useVbenModal({
|
|||||||
if (data?.values && data.update) {
|
if (data?.values && data.update) {
|
||||||
formApi.setValues({ ...data.values });
|
formApi.setValues({ ...data.values });
|
||||||
} else {
|
} else {
|
||||||
formApi.resetFields();
|
formApi.resetForm();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
28
apps/web-antd/src/views/system/dict/west-unit/api/index.ts
Normal file
28
apps/web-antd/src/views/system/dict/west-unit/api/index.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* 药品计量单位字典 API(yii_west_unit)
|
||||||
|
*/
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
const prefix = 'west-unit-dict/';
|
||||||
|
|
||||||
|
export async function getWestUnitDictList(data: any) {
|
||||||
|
return requestClient.get<any>(`${prefix}list`, { params: data });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getWestUnitDictOption() {
|
||||||
|
return requestClient.get<{ value: number; label: string }[]>(
|
||||||
|
`${prefix}option`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createWestUnitDict(data: Record<string, any>) {
|
||||||
|
return requestClient.post<any>(`${prefix}create`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateWestUnitDict(data: Record<string, any>) {
|
||||||
|
return requestClient.post<any>(`${prefix}update`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteWestUnitDict(data: { ids: number[] }) {
|
||||||
|
return requestClient.post<any>(`${prefix}delete`, data);
|
||||||
|
}
|
||||||
214
apps/web-antd/src/views/system/dict/west-unit/index.vue
Normal file
214
apps/web-antd/src/views/system/dict/west-unit/index.vue
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
/**
|
||||||
|
* 药品计量单位字典(yii_west_unit)
|
||||||
|
* 挂在字典管理下,供开方/仓库导入复用
|
||||||
|
*/
|
||||||
|
import type { VxeGridListeners } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import { TableAction } from '#/components/table-action';
|
||||||
|
|
||||||
|
import {
|
||||||
|
createWestUnitDict,
|
||||||
|
deleteWestUnitDict,
|
||||||
|
getWestUnitDictList,
|
||||||
|
updateWestUnitDict,
|
||||||
|
} from './api';
|
||||||
|
|
||||||
|
defineOptions({ name: 'WestUnitDict' });
|
||||||
|
|
||||||
|
const hasTopTableDropDownActions = ref(false);
|
||||||
|
const isUpdate = ref(false);
|
||||||
|
|
||||||
|
const formOptions = {
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
fieldName: 'keyword',
|
||||||
|
label: '关键字',
|
||||||
|
componentProps: { placeholder: '单位名称' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const gridOptions = {
|
||||||
|
checkboxConfig: { highlight: true, labelField: '' },
|
||||||
|
columns: [
|
||||||
|
{ type: 'checkbox', width: 60 },
|
||||||
|
{ field: 'id', title: 'ID', width: 80 },
|
||||||
|
{ field: 'name', title: '单位名称', minWidth: 180 },
|
||||||
|
{ field: 'created_at', title: '创建时间', width: 170 },
|
||||||
|
{ field: 'updated_at', title: '更新时间', width: 170 },
|
||||||
|
{ title: '操作', slots: { default: 'action' }, width: 140, fixed: 'right' },
|
||||||
|
],
|
||||||
|
keepSource: true,
|
||||||
|
pagerConfig: {},
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }: any, formValues: any) => {
|
||||||
|
return await getWestUnitDictList({
|
||||||
|
page: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
height: 'auto',
|
||||||
|
toolbarConfig: {
|
||||||
|
search: true,
|
||||||
|
refresh: true,
|
||||||
|
slots: { buttons: 'toolbar-buttons' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const gridEvents: VxeGridListeners<any> = {
|
||||||
|
checkboxChange() {
|
||||||
|
hasTopTableDropDownActions.value =
|
||||||
|
gridApi.grid.getCheckboxRecords().length > 0;
|
||||||
|
},
|
||||||
|
checkboxAll() {
|
||||||
|
hasTopTableDropDownActions.value =
|
||||||
|
gridApi.grid.getCheckboxRecords().length > 0;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions,
|
||||||
|
gridOptions: gridOptions as any,
|
||||||
|
gridEvents,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm({
|
||||||
|
wrapperClass: 'grid-cols-12',
|
||||||
|
commonConfig: {
|
||||||
|
formItemClass: 'col-span-12',
|
||||||
|
componentProps: { class: 'w-full' },
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
fieldName: 'id',
|
||||||
|
label: 'ID',
|
||||||
|
dependencies: { show: false, triggerFields: ['id'] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '单位名称',
|
||||||
|
rules: 'required',
|
||||||
|
componentProps: { placeholder: '如:盒、瓶、g、袋', maxlength: 20 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
showDefaultActions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [FormModal, formModalApi] = useVbenModal({
|
||||||
|
class: 'w-[480px]',
|
||||||
|
draggable: true,
|
||||||
|
onConfirm: async () => {
|
||||||
|
const e = await formApi.validate();
|
||||||
|
if (!e.valid) return;
|
||||||
|
const values = await formApi.getValues();
|
||||||
|
formModalApi.setState({ confirmLoading: true });
|
||||||
|
try {
|
||||||
|
if (isUpdate.value) {
|
||||||
|
await updateWestUnitDict(values);
|
||||||
|
} else {
|
||||||
|
await createWestUnitDict(values);
|
||||||
|
}
|
||||||
|
message.success('保存成功');
|
||||||
|
gridApi.query();
|
||||||
|
formModalApi.close();
|
||||||
|
} finally {
|
||||||
|
formModalApi.setState({ confirmLoading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
formApi.resetForm();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = formModalApi.getData<{ values?: any; update?: boolean }>();
|
||||||
|
isUpdate.value = !!data?.update;
|
||||||
|
if (data?.values && data.update) {
|
||||||
|
formApi.setValues({ ...data.values });
|
||||||
|
} else {
|
||||||
|
formApi.resetForm();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function showModal(row: any = {}, update = false) {
|
||||||
|
formModalApi.setData({ values: row, update });
|
||||||
|
formModalApi.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDelete(ids: number[]) {
|
||||||
|
await deleteWestUnitDict({ ids });
|
||||||
|
message.success('删除成功');
|
||||||
|
gridApi.query();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height title="单位管理">
|
||||||
|
<FormModal :title="`${isUpdate ? '编辑' : '新增'}单位`">
|
||||||
|
<Form />
|
||||||
|
</FormModal>
|
||||||
|
<Grid>
|
||||||
|
<template #toolbar-buttons>
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: '新增',
|
||||||
|
type: 'primary',
|
||||||
|
onClick: () => showModal({}, false),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '批量删除',
|
||||||
|
danger: true,
|
||||||
|
ifShow: () => hasTopTableDropDownActions,
|
||||||
|
popConfirm: {
|
||||||
|
title: '确认删除所选?被药品引用的单位无法删除。',
|
||||||
|
confirm: () => {
|
||||||
|
const ids = gridApi.grid
|
||||||
|
.getCheckboxRecords()
|
||||||
|
.map((r: any) => r.id);
|
||||||
|
handleDelete(ids);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #action="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: '编辑',
|
||||||
|
type: 'link',
|
||||||
|
onClick: () => showModal(row, true),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '删除',
|
||||||
|
type: 'link',
|
||||||
|
danger: true,
|
||||||
|
popConfirm: {
|
||||||
|
title: '确认删除该单位?',
|
||||||
|
confirm: () => handleDelete([row.id]),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
@@ -58,7 +58,7 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
onOpenChange(isOpen: boolean) {
|
onOpenChange(isOpen: boolean) {
|
||||||
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
|
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
formApi.resetFields();
|
formApi.resetForm();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { values, update } = modalApi.getData<Record<string, any>>() || {};
|
const { values, update } = modalApi.getData<Record<string, any>>() || {};
|
||||||
@@ -75,7 +75,7 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
: '',
|
: '',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
formApi.resetFields();
|
formApi.resetForm();
|
||||||
formApi.setValues({ sort: 0, status: 1, drugs_json: '' });
|
formApi.setValues({ sort: 0, status: 1, drugs_json: '' });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
} else {
|
} else {
|
||||||
// 新增时重置表单并设置默认值
|
// 新增时重置表单并设置默认值
|
||||||
isUpdate.value = false;
|
isUpdate.value = false;
|
||||||
formApi.resetFields();
|
formApi.resetForm();
|
||||||
formApi.setValues({
|
formApi.setValues({
|
||||||
id: '',
|
id: '',
|
||||||
title: '',
|
title: '',
|
||||||
@@ -83,7 +83,7 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 关闭时重置表单
|
// 关闭时重置表单
|
||||||
formApi.resetFields();
|
formApi.resetForm();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// 关闭时重置表单
|
// 关闭时重置表单
|
||||||
formApi.resetFields();
|
formApi.resetForm();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
} else {
|
} else {
|
||||||
// 新增时重置表单并设置默认值
|
// 新增时重置表单并设置默认值
|
||||||
isUpdate.value = false;
|
isUpdate.value = false;
|
||||||
formApi.resetFields();
|
formApi.resetForm();
|
||||||
formApi.setValues({
|
formApi.setValues({
|
||||||
id: '',
|
id: '',
|
||||||
drug_license_image: '',
|
drug_license_image: '',
|
||||||
@@ -85,7 +85,7 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 关闭时重置表单
|
// 关闭时重置表单
|
||||||
formApi.resetFields();
|
formApi.resetForm();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
} else {
|
} else {
|
||||||
// 新增时重置表单并设置默认值
|
// 新增时重置表单并设置默认值
|
||||||
isUpdate.value = false;
|
isUpdate.value = false;
|
||||||
formApi.resetFields();
|
formApi.resetForm();
|
||||||
formApi.setValues({
|
formApi.setValues({
|
||||||
id: '',
|
id: '',
|
||||||
name: '',
|
name: '',
|
||||||
@@ -79,7 +79,7 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 关闭时重置表单
|
// 关闭时重置表单
|
||||||
formApi.resetFields();
|
formApi.resetForm();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
onOpenChange(isOpen: boolean) {
|
onOpenChange(isOpen: boolean) {
|
||||||
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
|
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
formApi.resetFields();
|
formApi.resetForm();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { values, update } = modalApi.getData<Record<string, any>>() || {};
|
const { values, update } = modalApi.getData<Record<string, any>>() || {};
|
||||||
@@ -48,7 +48,7 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
if (values && update) {
|
if (values && update) {
|
||||||
formApi.setValues({ ...values });
|
formApi.setValues({ ...values });
|
||||||
} else {
|
} else {
|
||||||
formApi.resetFields();
|
formApi.resetForm();
|
||||||
formApi.setValues({ sort: 0, status: 1, description: '' });
|
formApi.setValues({ sort: 0, status: 1, description: '' });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
async onOpenChange(isOpen: boolean) {
|
async onOpenChange(isOpen: boolean) {
|
||||||
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
|
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
formApi.resetFields();
|
formApi.resetForm();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 打开时拉取功能字典,注入 permissions 下拉
|
// 打开时拉取功能字典,注入 permissions 下拉
|
||||||
@@ -71,7 +71,7 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
permissions: Array.isArray(values.permissions) ? values.permissions : [],
|
permissions: Array.isArray(values.permissions) ? values.permissions : [],
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
formApi.resetFields();
|
formApi.resetForm();
|
||||||
formApi.setValues({
|
formApi.setValues({
|
||||||
tier_group: 0,
|
tier_group: 0,
|
||||||
level_weight: 0,
|
level_weight: 0,
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
},
|
},
|
||||||
async onOpenChange(isOpen: boolean) {
|
async onOpenChange(isOpen: boolean) {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
formApi.resetFields();
|
formApi.resetForm();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const data = modalApi.getData<Record<string, any>>() || {};
|
const data = modalApi.getData<Record<string, any>>() || {};
|
||||||
|
|||||||
Reference in New Issue
Block a user