1. 修复了接诊页面的剂量、天数输入框过窄

This commit is contained in:
李琦
2026-07-15 08:46:31 +08:00
parent 316bbabcd1
commit 8eca0bd84d
66 changed files with 5242 additions and 393 deletions

View File

@@ -6,19 +6,20 @@
* - 支持中药/西药类型切换
* - 下拉选项展示:药品图片、名称、供应商、规格、价格
* - 选择时返回完整药品数据(包含默认用法字段)
* - isCard单选确认场景如仓绑药选中后以内嵌卡片展示已选药品
* @author 系统
* @date 2024
*/
import { ref, watch, onMounted, onUnmounted } from 'vue';
import { SearchOutlined, LoadingOutlined } from '@ant-design/icons-vue';
import { Input, Spin } from 'ant-design-vue';
import { Button, Input, Spin } from 'ant-design-vue';
import { debounce } from 'lodash-es';
import { getProductListDoctorReception } from '#/views/doctor/doctor-reception/api';
/** 中药无图时与仓库列表一致的占位图 */
const TCM_PLACEHOLDER_IMAGE =
/** 无图时占位(中药/西药等统一兜底,避免仓绑药下拉只剩文字) */
const DRUG_PLACEHOLDER_IMAGE =
'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk_upload_20250510/cd470ebf97e31c4048ba502ba71b66a3.jpg';
// ==================== Props 定义 ====================
@@ -29,12 +30,17 @@ interface Props {
* 1-中药2-西药
*/
type?: number;
/**
* 多药品类型(优先于 type有值时支持空关键词拉全量
* 例如 [2,4] 表示西药+中成药
*/
types?: number[];
/**
* 占位提示文本
*/
placeholder?: string;
/**
* 诊所ID
* 诊所ID(接诊/开方必传;仓药绑定等平台场景可省略,默认 0 走主库 types 检索)
*/
storeId?: number;
/**
@@ -45,14 +51,21 @@ interface Props {
* 是否禁用
*/
disabled?: boolean;
/**
* 是否以卡片展示已选药品(仓绑药等单选确认场景;开方默认 false
*/
isCard?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
type: 1,
types: () => [],
placeholder: '输入药品名称搜索',
storeId: 2,
// 默认 0平台无门店诊所场景由调用方传 :store-id
storeId: 0,
registerId: undefined,
disabled: false,
isCard: false,
});
// ==================== Emits 定义 ====================
@@ -97,10 +110,21 @@ const containerRef = ref<HTMLElement | null>(null);
*/
const highlightIndex = ref(-1);
/**
* isCard 模式下已选药品(用于输入框下方卡片)
*/
const selectedDrug = ref<any | null>(null);
/**
* 搜索框引用,便于「重新选择」时聚焦
*/
const inputRef = ref<{ focus?: () => void } | null>(null);
/**
* 解析下拉展示图:优先真图,无图用统一占位
*/
function resolveDropdownImage(item: any): string {
if (item._image) return item._image;
if (props.type === 1) return TCM_PLACEHOLDER_IMAGE;
return '';
return item._image || DRUG_PLACEHOLDER_IMAGE;
}
// ==================== 方法定义 ====================
@@ -108,10 +132,12 @@ function resolveDropdownImage(item: any): string {
/**
* 搜索药品
* @param keyword 搜索关键词
* @description 调用后端API搜索药品支持按名称和拼音搜索
* @description 调用后端API搜索药品支持按名称和拼音搜索
* 传入 types 时允许空关键词拉取该类型全量列表(用于下拉展开)
*/
const searchDrugs = debounce(async (keyword: string) => {
if (!keyword || keyword.length < 1) {
// 单类型模式:空关键词清空;多类型模式:空关键词仍请求后端拉全量
if ((!keyword || keyword.length < 1) && props.types.length === 0) {
searchResults.value = [];
showDropdown.value = false;
return;
@@ -121,37 +147,41 @@ const searchDrugs = debounce(async (keyword: string) => {
showDropdown.value = true;
try {
// types 优先:有多类型时只传 types不传 type避免后端被单 type 覆盖
const typeParams =
props.types.length > 0
? { types: props.types }
: { type: props.type };
const res = await getProductListDoctorReception({
name: keyword,
type: props.type,
name: keyword || '',
...typeParams,
store_id: props.storeId,
...(props.registerId ? { register_id: props.registerId } : {}),
});
// 处理返回数据
// 处理返回数据(兼容门店 relation 与平台主库打平结构)
if (Array.isArray(res)) {
searchResults.value = res.map((item: any) => ({
// 保留原始数据
...item,
// 提取常用字段便于访问
_id: item.id,
_drugId: item.drug_id || item.drug?.id,
_drugName: item.drug?.drug_name || item.drug_name || '',
_image: item.drug?.image || '',
_specification: item.drug?.specification || '',
_supplier: item.drug?.supplier?.name || '',
_price: item.price || 0,
// 默认用法字段
_timeId: item.drug?.time_id || 0,
_typeId: item.drug?.type_id || 0,
_frequencyId: item.drug?.frequency_id || 0,
_unitId: item.drug?.unit_id || 0,
_number: item.drug?.number || 1,
// 用法名称
_useNum: item.drug?.useNum,
_useType: item.drug?.useType,
_useFrequency: item.drug?.useFrequency,
}));
searchResults.value = res.map((item: any) => {
const drug = item.drug || {};
return {
...item,
_id: item.id,
_drugId: item.drug_id || drug.id || item.id,
_drugName: drug.drug_name || item.drug_name || '',
_image: drug.image || item.image || '',
_specification: drug.specification || item.specification || '',
_supplier: drug.supplier?.name || item.supplier?.name || '',
_price: Number(item.price ?? 0),
_timeId: drug.time_id || 0,
_typeId: drug.type_id || 0,
_frequencyId: drug.frequency_id || 0,
_unitId: drug.unit_id || 0,
_number: drug.number || 1,
_useNum: drug.useNum,
_useType: drug.useType,
_useFrequency: drug.useFrequency,
};
});
} else {
searchResults.value = [];
}
@@ -176,8 +206,14 @@ function handleInput(e: Event) {
/**
* 处理输入框获得焦点
* @description 多类型模式聚焦即拉取列表并展开;单类型模式仅在已有结果时展示下拉
*/
function handleFocus() {
if (props.types.length > 0) {
searchDrugs('');
showDropdown.value = true;
return;
}
if (searchResults.value.length > 0) {
showDropdown.value = true;
}
@@ -186,15 +222,36 @@ function handleFocus() {
/**
* 处理选中药品
* @param drug 选中的药品数据
* @description isCard 时保留 selectedDrug 用于卡片展示;否则清空选中态
*/
function handleSelectDrug(drug: any) {
emit('select', drug);
// 清空搜索状态
searchKeyword.value = '';
searchResults.value = [];
showDropdown.value = false;
highlightIndex.value = -1;
if (props.isCard) {
selectedDrug.value = drug;
} else {
selectedDrug.value = null;
}
}
/**
* 清空已选卡片并重新搜索(仅 isCard
* 同步 emit null便于父级清空 drug_id
*/
function handleReselect() {
selectedDrug.value = null;
searchKeyword.value = '';
searchResults.value = [];
showDropdown.value = false;
highlightIndex.value = -1;
emit('select', null);
// 下一帧聚焦搜索框,方便重新选药
setTimeout(() => {
inputRef.value?.focus?.();
}, 0);
}
/**
@@ -261,6 +318,7 @@ watch(
searchResults.value = [];
showDropdown.value = false;
searchKeyword.value = '';
selectedDrug.value = null;
},
);
</script>
@@ -269,6 +327,7 @@ watch(
<div ref="containerRef" class="drug-search-select">
<!-- 搜索输入框 -->
<Input
ref="inputRef"
v-model:value="searchKeyword"
:placeholder="placeholder"
:disabled="disabled"
@@ -283,6 +342,49 @@ watch(
</template>
</Input>
<!-- isCard已选药品卡片与下拉项同结构带边框区分 -->
<div
v-if="isCard && selectedDrug"
class="drug-item drug-item--selected-card"
>
<div class="drug-item__image">
<img
:src="resolveDropdownImage(selectedDrug)"
alt=""
class="drug-item__img"
@error="
(e) => ((e.target as HTMLImageElement).src = DRUG_PLACEHOLDER_IMAGE)
"
/>
</div>
<div class="drug-item__info">
<div class="drug-item__name">{{ selectedDrug._drugName }}</div>
<div class="drug-item__meta">
<span class="drug-item__id">ID{{ selectedDrug._drugId }}</span>
<span v-if="selectedDrug._specification" class="drug-item__spec">
规格{{ selectedDrug._specification }}
</span>
<span v-if="selectedDrug._supplier" class="drug-item__supplier">
供应商{{ selectedDrug._supplier }}
</span>
</div>
</div>
<div class="drug-item__price">
<template v-if="Number(selectedDrug._price) > 0">
¥{{ Number(selectedDrug._price).toFixed(2) }}
</template>
<span v-else class="drug-item__price--empty">暂无</span>
</div>
<Button
type="link"
size="small"
class="drug-item__reselect"
@click="handleReselect"
>
重新选择
</Button>
</div>
<!-- 下拉列表 -->
<div v-if="showDropdown" class="drug-dropdown">
<!-- 加载中 -->
@@ -321,12 +423,11 @@ watch(
<div v-else class="drug-item__img-placeholder">无图</div>
</div>
<!-- 药品信息 -->
<!-- 药品信息药名 / ID / 规格 / 供应商 -->
<div class="drug-item__info">
<!-- 药品名称 -->
<div class="drug-item__name">{{ item._drugName }}</div>
<!-- 规格和供应商 -->
<div class="drug-item__meta">
<span class="drug-item__id">ID{{ item._drugId }}</span>
<span v-if="item._specification" class="drug-item__spec">
规格{{ item._specification }}
</span>
@@ -336,9 +437,12 @@ watch(
</div>
</div>
<!-- 价格 -->
<!-- 价格无价平台主库常见展示暂无 -->
<div class="drug-item__price">
¥{{ Number(item._price).toFixed(2) }}
<template v-if="Number(item._price) > 0">
¥{{ Number(item._price).toFixed(2) }}
</template>
<span v-else class="drug-item__price--empty">暂无</span>
</div>
</div>
</div>
@@ -347,6 +451,7 @@ watch(
</template>
<style lang="scss" scoped>
/* 只用框架主题变量,随亮暗/主题色自动适配,不写 .dark 硬编码覆盖 */
.drug-search-select {
position: relative;
width: 100%;
@@ -359,8 +464,8 @@ watch(
right: 0;
z-index: 1050;
margin-top: 4px;
background: #fff;
border: 1px solid #e5e7eb;
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 8px;
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
max-height: 400px;
@@ -373,7 +478,7 @@ watch(
justify-content: center;
gap: 8px;
padding: 24px;
color: #999;
color: hsl(var(--muted-foreground));
font-size: 14px;
}
@@ -387,7 +492,7 @@ watch(
}
&::-webkit-scrollbar-thumb {
background: #d9d9d9;
background: hsl(var(--border));
border-radius: 3px;
}
@@ -407,7 +512,25 @@ watch(
&:hover,
&--active {
background-color: #f5f7fa;
background-color: hsl(var(--accent-hover));
}
/* 已选确认卡片:固定展示,非下拉浮层 */
&--selected-card {
margin-top: 8px;
cursor: default;
background: hsl(var(--muted) / 0.3);
border: 1px solid hsl(var(--border));
border-radius: 8px;
&:hover {
background: hsl(var(--muted) / 0.3);
}
}
&__reselect {
flex-shrink: 0;
padding: 0 4px;
}
&__image {
@@ -421,7 +544,7 @@ watch(
height: 48px;
object-fit: cover;
border-radius: 6px;
border: 1px solid #f0f0f0;
border: 1px solid hsl(var(--border));
}
&__img-placeholder {
@@ -430,10 +553,10 @@ watch(
display: flex;
align-items: center;
justify-content: center;
background: #f5f5f5;
background: hsl(var(--accent));
border-radius: 6px;
font-size: 12px;
color: #bbb;
color: hsl(var(--muted-foreground));
}
&__info {
@@ -445,7 +568,7 @@ watch(
&__name {
font-size: 14px;
font-weight: 500;
color: #333;
color: hsl(var(--foreground));
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@@ -457,57 +580,31 @@ watch(
flex-wrap: wrap;
gap: 12px;
font-size: 12px;
color: #999;
color: hsl(var(--muted-foreground));
}
&__id {
color: hsl(var(--muted-foreground));
}
&__spec {
color: #666;
color: hsl(var(--muted-foreground));
}
&__supplier {
color: #1890ff;
color: hsl(var(--primary));
}
&__price {
flex-shrink: 0;
font-size: 16px;
font-weight: 600;
color: #ff4d4f;
}
}
color: hsl(var(--destructive));
/* 暗色模式适配 */
.dark {
.drug-dropdown {
background: #1f2937;
border-color: #374151;
}
.drug-item {
&:hover,
&--active {
background-color: #374151;
}
&__img-placeholder {
background: #374151;
color: #6b7280;
}
&__name {
color: #f3f4f6;
}
&__meta {
color: #9ca3af;
}
&__spec {
color: #d1d5db;
}
&__supplier {
color: #60a5fa;
&--empty {
font-size: 13px;
font-weight: 400;
color: hsl(var(--muted-foreground));
}
}
}

View File

@@ -4,6 +4,7 @@ import { computed, nextTick, onMounted, ref, watch } from 'vue';
import { useVModel } from '@vueuse/core';
import { Empty, Input, Popover, Spin } from 'ant-design-vue';
import { matchExpressByTrackingNo } from '#/utils/matchExpressByTrackingNo';
import { getExpressCompaniesOption } from '#/views/business/express/express-company/api';
defineOptions({
@@ -15,6 +16,8 @@ const props = defineProps<{
value?: string;
disabled?: boolean;
placeholder?: string;
/** 运单号:变化时按前缀自动匹配快递公司(用户手动改选后不再覆盖) */
trackingNo?: string;
}>();
const emits = defineEmits<{
@@ -34,6 +37,8 @@ const loading = ref(false);
const searchKeyword = ref('');
const options = ref<ExpressCompanyOption[]>([]);
const searchInputRef = ref<InstanceType<typeof Input> | null>(null);
/** 用户是否已手动选择/清空;为 true 时不再按单号自动覆盖 */
const userPicked = ref(false);
function filterExpressCompanyOptions(keyword: string, list: ExpressCompanyOption[]) {
const q = keyword.trim().toLowerCase();
@@ -59,17 +64,36 @@ const displayText = computed(() => {
return item.code ? `${item.name || '-'}${item.code}` : (item.name || '-');
});
/**
* 根据 trackingNo 自动选中快递公司
* 规则:尚未手动改选,或当前选中仍与规则结果一致时才覆盖
*/
function tryAutoMatchByTrackingNo() {
if (!options.value.length) return;
const matched = matchExpressByTrackingNo(props.trackingNo, options.value);
if (!matched?.code) return;
const matchedCode = matched.code;
if (userPicked.value && mValue.value && mValue.value !== matchedCode) {
// 用户已选其他公司,不覆盖
return;
}
if (mValue.value === matchedCode) return;
mValue.value = matchedCode;
}
async function loadOptions() {
loading.value = true;
try {
const res = await getExpressCompaniesOption({});
options.value = Array.isArray(res) ? res : [];
tryAutoMatchByTrackingNo();
} finally {
loading.value = false;
}
}
function selectOption(item: ExpressCompanyOption) {
userPicked.value = true;
mValue.value = item.code;
open.value = false;
searchKeyword.value = '';
@@ -77,6 +101,7 @@ function selectOption(item: ExpressCompanyOption) {
function clearSelection(e: Event) {
e.stopPropagation();
userPicked.value = true;
mValue.value = undefined;
}
@@ -99,6 +124,14 @@ watch(
},
);
watch(
() => props.trackingNo,
() => {
// 单号变化时允许再自动匹配(除非用户已改选为不一致公司)
tryAutoMatchByTrackingNo();
},
);
onMounted(() => {
loadOptions();
});
@@ -177,7 +210,7 @@ onMounted(() => {
.clear-btn {
pointer-events: auto;
cursor: pointer;
color: #86909c;
color: hsl(var(--muted-foreground));
font-size: 16px;
line-height: 1;
padding: 0 4px;
@@ -203,10 +236,11 @@ onMounted(() => {
text-align: left;
cursor: pointer;
border-radius: 6px;
color: hsl(var(--foreground));
}
.express-item:hover,
.express-item.active {
background: #f2f3f5;
background: hsl(var(--accent));
}
.express-meta {
min-width: 0;
@@ -214,11 +248,11 @@ onMounted(() => {
}
.express-name {
font-size: 14px;
color: #1d2129;
color: hsl(var(--foreground));
}
.express-sub {
font-size: 12px;
color: #86909c;
color: hsl(var(--muted-foreground));
}
.express-empty {
margin: 12px 0;
@@ -228,5 +262,8 @@ onMounted(() => {
<style>
.express-company-select-popover .ant-popover-inner {
padding: 12px;
background: hsl(var(--card));
color: hsl(var(--foreground));
border: 1px solid hsl(var(--border));
}
</style>

View File

@@ -912,6 +912,7 @@ export const usePrescriptionStore = defineStore('prescription', () => {
doctorSecondSignValue = 0,
customSendMode: number = 0,
customStoreId: number | null = null,
warehouseMap: Record<number, number> | null = null,
) => {
if (currentDrugs.value.length === 0) {
message.error('请选择药品');
@@ -986,6 +987,8 @@ export const usePrescriptionStore = defineStore('prescription', () => {
// 诊所选择参数转诊挂号时自动使用承接方诊所ID
send_mode: finalSendMode,
custom_store_id: finalSendMode === 1 ? finalStoreId : null,
// 药店按药选配送仓映射 drug_id -> warehouse_id
warehouse_map: warehouseMap || undefined,
});
const res = await response;

View File

@@ -0,0 +1,66 @@
/**
* 根据运单号前缀推断快递公司(用于发货表单自动选中)
* 匹配关键字同时支持公司名与 code如 shunfeng
*/
export type ExpressMatchOption = {
id?: number;
name?: string;
code?: string;
};
/** 常见单号前缀 → 公司名 / code 关键字(按前缀长度降序匹配,避免短前缀误伤) */
const PREFIX_RULES: Array<{ prefixes: string[]; keywords: string[] }> = [
{ prefixes: ['SF'], keywords: ['顺丰', 'shunfeng', 'sf'] },
{ prefixes: ['ZTO', 'ZT'], keywords: ['中通', 'zhongtong', 'zto'] },
{ prefixes: ['STO'], keywords: ['申通', 'shentong', 'sto'] },
{ prefixes: ['YTO', 'YT'], keywords: ['圆通', 'yuantong', 'yto'] },
{ prefixes: ['YD'], keywords: ['韵达', 'yunda', 'yd'] },
{ prefixes: ['JD'], keywords: ['京东', 'jd', 'jingdong'] },
{ prefixes: ['JT'], keywords: ['极兔', 'jitu', 'jt'] },
{ prefixes: ['EMS', 'E'], keywords: ['ems', '邮政'] },
{ prefixes: ['HHTT', 'HT'], keywords: ['百世', 'baishi', 'huitong'] },
{ prefixes: ['UC'], keywords: ['优速', 'uc'] },
{ prefixes: ['DBL'], keywords: ['德邦', 'debang', 'dbl'] },
];
/**
* 按运单号前缀在 options 中找最可能的快递公司
* @returns 匹配到的 option未匹配返回 null
*/
export function matchExpressByTrackingNo(
trackingNo: string | undefined | null,
options: ExpressMatchOption[],
): ExpressMatchOption | null {
const no = String(trackingNo || '')
.trim()
.toUpperCase()
.replace(/\s+/g, '');
if (!no || !Array.isArray(options) || !options.length) {
return null;
}
const sortedRules = [...PREFIX_RULES].sort(
(a, b) =>
Math.max(...b.prefixes.map((p) => p.length)) -
Math.max(...a.prefixes.map((p) => p.length)),
);
let matchedKeywords: string[] | null = null;
for (const rule of sortedRules) {
if (rule.prefixes.some((p) => no.startsWith(p))) {
matchedKeywords = rule.keywords;
break;
}
}
if (!matchedKeywords) {
return null;
}
const keywords = matchedKeywords.map((k) => k.toLowerCase());
for (const opt of options) {
const name = String(opt.name || '').toLowerCase();
const code = String(opt.code || '').toLowerCase();
if (keywords.some((k) => name.includes(k) || code === k || code.includes(k))) {
return opt;
}
}
return null;
}

View File

@@ -56,6 +56,8 @@ import InfoModal from '#/components/modal/InfoModal.vue';
import { DrugSearchSelect } from '#/components/drug-search-select';
import { formatPriceDisplay } from '#/utils/formatPrice';
import ChineseMedicineConfig from '#/views/doctor/doctor-reception/components/ChineseMedicineConfig.vue';
import { getDeliveryWarehouseOptionsByDrugs } from '#/views/doctor/doctor-reception/api';
import WarehouseSelectModal from './WarehouseSelectModal.vue';
const splitString = (input: string) => input.split(',');
@@ -210,6 +212,77 @@ const handleCheckChineseMedicineConflict = async () => {
}
};
// 药店开方待选仓时暂存发送参数
const pendingSendParams = ref<{
doctorSecondSignValue: number;
sendMode: number;
customStoreId: number | null;
} | null>(null);
const [WarehouseSelectModals, warehouseSelectModalApi] = useVbenModal({
connectedComponent: WarehouseSelectModal,
});
/**
* 药店在线问诊:若药品有配送仓绑定,则先弹选仓再发送
*/
const tryPharmacyWarehouseSelect = async (
doctorSecondSignValue: number,
sendMode: number,
customStoreId: number | null,
): Promise<boolean> => {
const storagePrefix =
modalData.value?.storagePrefix || 'onlineConsultation-';
// 诊所在线复诊前缀不走手动选仓(后端自动按报价)
if (storagePrefix === 'onlineConsultationClinic-') {
return false;
}
const drugs = prescriptionStore.currentDrugs || [];
const drugIds = drugs.map((d: any) => Number(d.id)).filter((id: number) => id > 0);
if (drugIds.length === 0) {
return false;
}
const needQtyMap: Record<number, number> = {};
drugs.forEach((d: any) => {
needQtyMap[Number(d.id)] = Number(d.select_number || d.number || 1);
});
const optionsMap = await getDeliveryWarehouseOptionsByDrugs({
drug_ids: drugIds.join(','),
need_qty_map: needQtyMap,
});
const rows: any[] = [];
Object.keys(optionsMap || {}).forEach((drugId) => {
const list = optionsMap[drugId] || [];
if (!list.length) return;
const drug = drugs.find((d: any) => Number(d.id) === Number(drugId));
rows.push({
drug_id: Number(drugId),
drug_name: drug?.drug_name || drug?.name || `药品#${drugId}`,
options: list,
});
});
if (rows.length === 0) {
return false;
}
pendingSendParams.value = { doctorSecondSignValue, sendMode, customStoreId };
warehouseSelectModalApi.setData({ rows });
warehouseSelectModalApi.open();
return true;
};
const onWarehouseSelected = async (warehouseMap: Record<number, number>) => {
if (!pendingSendParams.value) return;
const { doctorSecondSignValue, sendMode, customStoreId } =
pendingSendParams.value;
pendingSendParams.value = null;
await executeSendPrescription(
doctorSecondSignValue,
sendMode,
customStoreId,
warehouseMap,
);
};
// 处方发送前的确认流程
const handleSendPrescription = async (doctorSecondSignValue = 0) => {
// 确保基础数据已初始化(包括当前诊所列表)
@@ -219,14 +292,24 @@ const handleSendPrescription = async (doctorSecondSignValue = 0) => {
await prescriptionStore.fetchRegisterStoreInfo();
const storeInfo = prescriptionStore.registerStoreInfo;
let sendMode = 0;
let customStoreId: number | null = null;
if (storeInfo?.is_from_transfer === 1 && storeInfo?.delegate_store_id) {
await executeSendPrescription(doctorSecondSignValue, 1, storeInfo.delegate_store_id);
sendMode = 1;
customStoreId = storeInfo.delegate_store_id;
} else if (storeInfo?.store_id) {
await executeSendPrescription(doctorSecondSignValue, 1, storeInfo.store_id);
} else {
await executeSendPrescription(doctorSecondSignValue, 0, null);
sendMode = 1;
customStoreId = storeInfo.store_id;
}
const needSelect = await tryPharmacyWarehouseSelect(
doctorSecondSignValue,
sendMode,
customStoreId,
);
if (needSelect) {
return;
}
await executeSendPrescription(doctorSecondSignValue, sendMode, customStoreId);
};
// 执行发送处方
@@ -234,11 +317,13 @@ const executeSendPrescription = async (
doctorSecondSignValue: number,
sendMode: number,
customStoreId: number | null,
warehouseMap: Record<number, number> | null = null,
) => {
const result = await prescriptionStore.sendPrescription(
doctorSecondSignValue,
sendMode,
customStoreId,
warehouseMap,
);
// 检查返回结果可能是boolean或包含转诊信息的对象
@@ -1505,6 +1590,8 @@ const cancelSaveCommonPrescription = () => {
<PrescriptionDetailModal />
<!-- 常用方选择弹窗 -->
<CommonPrescriptionModals />
<!-- 药店开方选配送仓库 -->
<WarehouseSelectModals @confirm="onWarehouseSelected" />
</Modal>
</template>

View File

@@ -0,0 +1,92 @@
<script lang="ts" setup>
/**
* 药店开方:按药品选择配送仓库
* 交互尽量简单:每个药品一个下拉,确认后回传 warehouse_map
*/
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message, Select } from 'ant-design-vue';
const emit = defineEmits<{
(e: 'confirm', map: Record<number, number>): void;
}>();
const drugRows = ref<
Array<{
drug_id: number;
drug_name: string;
options: Array<{
warehouse_id: number;
warehouse_name: string;
quote: string;
available_stock: number;
}>;
warehouse_id?: number;
}>
>([]);
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
draggable: true,
onCancel() {
modalApi.close();
},
onConfirm() {
const map: Record<number, number> = {};
for (const row of drugRows.value) {
if (!row.warehouse_id) {
message.error(`请为【${row.drug_name}】选择配送仓库`);
return;
}
map[row.drug_id] = row.warehouse_id;
}
emit('confirm', map);
modalApi.close();
},
onOpenChange(isOpen: boolean) {
if (isOpen) {
const data = modalApi.getData<{ rows: typeof drugRows.value }>();
drugRows.value = (data?.rows || []).map((row) => ({
...row,
warehouse_id: row.options?.[0]?.warehouse_id,
}));
}
},
});
const canConfirm = computed(() =>
drugRows.value.every((row) => !!row.warehouse_id),
);
</script>
<template>
<Modal title="选择配送仓库" class="w-[520px]" :confirm-disabled="!canConfirm">
<div class="space-y-3 py-2">
<div
v-for="row in drugRows"
:key="row.drug_id"
class="flex items-center gap-3"
>
<div class="w-40 shrink-0 truncate" :title="row.drug_name">
{{ row.drug_name }}
</div>
<Select
v-model:value="row.warehouse_id"
class="flex-1"
placeholder="请选择配送仓库"
:options="
row.options.map((o) => ({
label: `${o.warehouse_name}(报价${o.quote},库存${o.available_stock}`,
value: o.warehouse_id,
}))
"
/>
</div>
<div v-if="drugRows.length === 0" class="text-gray-400">
当前药品无需选择配送仓库
</div>
</div>
</Modal>
</template>

View File

@@ -249,7 +249,7 @@ function prescriptionStatusColor() {
{{ data.trans_expenses }}
</Descriptions.Item>
<Descriptions.Item label="是否免邮">
{{ data.free_ship === 1 ? '是' : '否' }}
{{ data.free_ship === 0 ? '是' : '否' }}
</Descriptions.Item>
<Descriptions.Item label="状态">
<Tag v-if="data.status === 0" color="red">待支付</Tag>

View File

@@ -40,6 +40,16 @@ export const modalFormProps: VbenFormProps = {
formItemClass: 'col-span-6',
label: '快递公司',
rules: 'required',
// 单号变化时传入 trackingNo组件按前缀自动匹配快递公司
dependencies: {
triggerFields: ['express_no'],
componentProps(values) {
return {
placeholder: '请选择快递公司',
trackingNo: values.express_no,
};
},
},
},
{
component: 'Textarea',

View File

@@ -37,6 +37,12 @@ export const gridOptions: VxeGridProps<RowType> = {
width: 200,
slots: { default: 'express-info' },
},
{
field: 'delivery_warehouses',
title: '配送仓库',
width: 160,
slots: { default: 'delivery-warehouses' },
},
{
field: 'delivery_method',
title: '订单类型&邮寄方式&订单状态',

View File

@@ -643,6 +643,22 @@ const openOrderAmountVerify = () => {
<div class="text-xs text-gray-500">{{ formatExpressAddress(row) }}</div>
</div>
</template>
<!-- 患者配送仓库一单多仓用 Tag 展示 -->
<template #delivery-warehouses="{ row }">
<div
v-if="Array.isArray(row.delivery_warehouses) && row.delivery_warehouses.length"
class="flex flex-wrap gap-1"
>
<Tag
v-for="wh in row.delivery_warehouses"
:key="wh.id"
color="processing"
>
{{ wh.name }}
</Tag>
</div>
<span v-else class="text-[hsl(var(--muted-foreground))]">-</span>
</template>
<template #price-info="{ row }">
<div class="space-y-0.5 text-sm leading-snug">
<div>

View File

@@ -0,0 +1,110 @@
<script lang="ts" setup>
/**
* 分类仓储合并列:分区/分类 + 配送仓绑定 + 总仓是否入库
* Tags 打开绑定列表;「新增绑定」直接走新增表单
*/
import { Tag } from 'ant-design-vue';
defineProps<{
row: Record<string, any>;
}>();
const emit = defineEmits<{
category: [row: Record<string, any>];
bind: [row: Record<string, any>];
bindCreate: [row: Record<string, any>];
stockIn: [row: Record<string, any>];
}>();
</script>
<template>
<div class="category-warehouse-cell">
<div class="category-warehouse-cell__row">
<span class="category-warehouse-cell__label">分区</span>
<a
class="text-primary cursor-pointer hover:underline"
@click.stop="emit('category', row)"
>
{{ row.zone_name || '未设置' }}
</a>
</div>
<div class="category-warehouse-cell__row">
<span class="category-warehouse-cell__label">分类</span>
<a
class="text-primary cursor-pointer hover:underline"
@click.stop="emit('category', row)"
>
{{ row.category_name || '未设置' }}
</a>
</div>
<div class="category-warehouse-cell__row">
<span class="category-warehouse-cell__label">配送仓</span>
<div class="category-warehouse-cell__wh">
<a
class="text-primary cursor-pointer hover:underline"
@click.stop="emit('bind', row)"
>
<template
v-if="row.delivery_warehouses && row.delivery_warehouses.length"
>
<Tag
v-for="w in row.delivery_warehouses"
:key="w.id"
class="mb-1 mr-1"
color="blue"
>
{{ w.name }}
</Tag>
</template>
<span v-else class="category-warehouse-cell__muted">未绑定</span>
</a>
<a
class="text-primary cursor-pointer hover:underline category-warehouse-cell__add"
@click.stop="emit('bindCreate', row)"
>
新增绑定
</a>
</div>
</div>
<div class="category-warehouse-cell__row">
<span class="category-warehouse-cell__label">入库</span>
<Tag v-if="row.in_central_warehouse" color="green">已入库</Tag>
<a v-else @click.stop="emit('stockIn', row)">
<Tag color="orange" class="cursor-pointer">未入库</Tag>
</a>
</div>
</div>
</template>
<style scoped>
.category-warehouse-cell {
display: flex;
flex-direction: column;
gap: 4px;
padding: 4px 0;
font-size: 13px;
}
.category-warehouse-cell__row {
display: flex;
align-items: flex-start;
gap: 8px;
line-height: 1.4;
}
.category-warehouse-cell__label {
flex-shrink: 0;
width: 42px;
color: hsl(var(--muted-foreground));
}
.category-warehouse-cell__muted {
color: hsl(var(--muted-foreground));
}
.category-warehouse-cell__wh {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
min-width: 0;
}
.category-warehouse-cell__add {
flex-shrink: 0;
font-size: 12px;
}
</style>

View File

@@ -0,0 +1,38 @@
<script lang="ts" setup>
/**
* 总仓售价列:已入库可点开调价;未入库提示并可触发入库
*/
defineProps<{
row: Record<string, any>;
}>();
const emit = defineEmits<{
adjust: [row: Record<string, any>];
stockIn: [row: Record<string, any>];
}>();
function formatPrice(v: unknown) {
const n = Number(v);
if (!(n >= 0) || Number.isNaN(n)) return '-';
return `${n.toFixed(2)}`;
}
</script>
<template>
<div>
<a
v-if="row.in_central_warehouse"
class="text-primary cursor-pointer hover:underline"
@click.stop="emit('adjust', row)"
>
{{ formatPrice(row.central_price) }}
</a>
<a
v-else
class="cursor-pointer"
style="color: hsl(var(--muted-foreground))"
@click.stop="emit('stockIn', row)"
>
未入库
</a>
</div>
</template>

View File

@@ -0,0 +1,114 @@
<script lang="ts" setup>
/**
* 总仓入库弹窗(从药品列表「未入库」打开)
* 药品已预填,只填供货价/销售价后 createWarehouseDrugManagement
*/
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createWarehouseDrugManagement } from '#/views/business/warehouse-drug-management/admin/api';
const drugId = ref(0);
const drugName = ref('');
const productType = ref(2);
const productGridApi = ref();
const [Form, formApi] = useVbenForm({
wrapperClass: 'grid-cols-12',
commonConfig: {
formItemClass: 'col-span-12',
componentProps: { class: 'w-full' },
},
layout: 'horizontal',
showDefaultActions: false,
schema: [
{
component: 'VbenInput',
fieldName: 'drug_id',
label: '药品ID',
componentProps: { disabled: true },
},
{
component: 'VbenInput',
fieldName: 'drug_name',
label: '药品名称',
componentProps: { disabled: true },
},
{
component: 'InputNumber',
fieldName: 'market_price',
label: '供货价格',
rules: 'required',
componentProps: {
min: 0,
precision: 4,
class: 'w-full',
placeholder: '请输入供货价格',
},
},
{
component: 'InputNumber',
fieldName: 'price',
label: '销售价格',
rules: 'required',
componentProps: {
min: 0,
precision: 4,
class: 'w-full',
placeholder: '请输入销售价格',
},
},
],
});
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
draggable: true,
onCancel() {
modalApi.close();
},
onConfirm: async () => {
const result = await formApi.validate();
if (!result.valid) return;
const values = await formApi.getValues();
modalApi.setState({ loading: true, confirmLoading: true });
try {
// 总仓入库接口必填drug_id / market_price / pricetype 由药品本身决定)
await createWarehouseDrugManagement({
drug_id: drugId.value,
market_price: values.market_price,
price: values.price,
});
message.success('入库成功');
productGridApi.value?.query?.();
modalApi.close();
} finally {
modalApi.setState({ loading: false, confirmLoading: false });
}
},
onOpenChange(isOpen: boolean) {
if (!isOpen) return;
const data = modalApi.getData<Record<string, any>>() || {};
drugId.value = Number(data.drug_id || data.values?.id || 0);
drugName.value =
data.drug_name || data.values?.drug_name || `药品#${drugId.value}`;
productType.value = Number(data.productType || data.type || 2);
productGridApi.value = data.gridApi;
formApi.setValues({
drug_id: drugId.value,
drug_name: drugName.value,
market_price: undefined,
price: undefined,
});
},
});
</script>
<template>
<Modal :title="`总仓入库 - ${drugName}`" class="w-[420px]">
<Form />
</Modal>
</template>

View File

@@ -0,0 +1,116 @@
<script lang="ts" setup>
/**
* 商品信息合并列:图片 + 名称/ID/拼音/发布时间 + 说明书预览链接
* 五类商品列表复用,避免每页各写一套 slot
*/
import { computed, ref } from 'vue';
import { Image } from 'ant-design-vue';
const props = defineProps<{
row: Record<string, any>;
}>();
const previewVisible = ref(false);
const instructionUrl = computed(() => {
const url = props.row?.instruction;
return typeof url === 'string' && url.trim() ? url.trim() : '';
});
function openInstruction() {
if (!instructionUrl.value) return;
previewVisible.value = true;
}
</script>
<template>
<div class="product-info-cell">
<Image
v-if="row.image"
:src="row.image"
:width="40"
:height="40"
class="product-info-cell__img"
/>
<div v-else class="product-info-cell__img product-info-cell__img--empty">
无图
</div>
<div class="product-info-cell__body">
<div class="product-info-cell__name">{{ row.drug_name || '-' }}</div>
<div class="product-info-cell__meta">
<span>ID{{ row.id }}</span>
<span v-if="row.pinyin_simple">拼音{{ row.pinyin_simple }}</span>
<span v-if="row.created_at">发布时间{{ row.created_at }}</span>
</div>
<a
v-if="instructionUrl"
class="product-info-cell__link text-primary"
@click.stop="openInstruction"
>
说明书
</a>
<!-- 隐藏 Image 仅用于说明书预览 -->
<Image
v-if="instructionUrl"
:src="instructionUrl"
:style="{ display: 'none' }"
:preview="{
visible: previewVisible,
onVisibleChange: (v: boolean) => (previewVisible = v),
}"
/>
</div>
</div>
</template>
<style scoped>
.product-info-cell {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 4px 0;
}
.product-info-cell__img {
flex-shrink: 0;
border-radius: 4px;
object-fit: cover;
}
.product-info-cell__img--empty {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
color: hsl(var(--muted-foreground));
background: hsl(var(--muted) / 0.3);
border: 1px solid hsl(var(--border));
border-radius: 4px;
}
.product-info-cell__body {
min-width: 0;
flex: 1;
}
.product-info-cell__name {
font-weight: 500;
color: hsl(var(--foreground));
line-height: 1.4;
word-break: break-all;
}
.product-info-cell__meta {
display: flex;
flex-wrap: wrap;
gap: 6px 12px;
margin-top: 4px;
font-size: 12px;
color: hsl(var(--muted-foreground));
}
.product-info-cell__link {
display: inline-block;
margin-top: 4px;
font-size: 12px;
cursor: pointer;
}
.product-info-cell__link:hover {
text-decoration: underline;
}
</style>

View File

@@ -8,114 +8,86 @@ import { getHealthFoodList } from '../api';
* 表格行数据类型定义
*/
interface RowType {
id: string; // 保健食品ID
name: string; // 保健食品名称
logo: string; // 保健食品Logo
introduce: string; // 保健食品介绍
created_at: string; // 创建时间
zone_id: number; // 分区ID
zone_name: string; // 分区名称
category_id: number; // 分类ID
category_name: string; // 分类名称
id: string;
name: string;
logo: string;
introduce: string;
created_at: string;
zone_id: number;
zone_name: string;
category_id: number;
category_name: string;
}
/**
* 保健食品管理表格配置
* 定义表格的列、分页、查询等配置
* 列结构对齐西药:商品信息/分类仓储/总仓售价合并列 + 页内特有列
*/
export const gridOptions: VxeGridProps<RowType> = {
// 复选框配置
checkboxConfig: {
highlight: true, // 高亮选中行
labelField: '', // 标签字段
highlight: true,
labelField: '',
},
// 列配置
columnConfig: {
useKey: true, // 使用key作为列的唯一标识
useKey: true,
},
// 行配置
rowConfig: {
useKey: true, // 使用key作为行的唯一标识
useKey: true,
},
// 表格列定义数组
columns: [
{ type: 'checkbox', width: 60 }, // 复选框列
{ field: 'id', align: 'left', title: 'ID', width: 100 }, // ID列
{ field: 'drug_name', align: 'left', title: '保健食品名称' }, // 保健食品名称列
{ field: 'pinyin_simple', title: '拼音' }, // 拼音列
{ type: 'checkbox', width: 60 },
{
field: 'drug_name',
align: 'left',
title: '商品信息',
minWidth: 260,
slots: { default: 'product_info' },
},
{
field: 'zone_name',
title: '分',
width: 100,
slots: { default: 'zone' }, // 使用插槽显示可点击的分区链接
title: '分类仓储',
minWidth: 200,
slots: { default: 'category_warehouse' },
},
{
field: 'category_name',
title: '分类',
width: 120,
slots: { default: 'category' }, // 使用插槽显示可点击的分类链接
field: 'central_price',
title: '总仓售价',
width: 110,
slots: { default: 'central_price' },
},
{ field: 'supplier.name', title: '供应商', slots: { default: 'supplier' } }, // 供应商列,使用插槽自定义显示
{
field: 'image',
align: 'left',
title: '产品图片',
slots: { default: 'image' }, // 使用插槽显示图片
width: 130,
},
{
field: 'instruction',
align: 'left',
title: '说明书',
slots: { default: 'instruction' }, // 使用插槽显示说明书
width: 130,
},
// 注意已移除function列主要功能因为保健食品不需要功效字段
{ field: 'specification', title: '规格' }, // 规格列
{ field: 'status', title: '状态', slots: { default: 'status' } }, // 状态列,使用插槽显示标签
{ field: 'created_at', title: '发布时间' }, // 发布时间列
{ type: 'html', title: '操作', width: 230, slots: { default: 'action' } }, // 操作列,使用插槽显示操作按钮
{ field: 'supplier.name', title: '供应商', slots: { default: 'supplier' }, width: 130 },
{ field: 'specification', title: '规格', width: 120 },
{ field: 'status', title: '状态', width: 90, slots: { default: 'status' } },
{ type: 'html', title: '操作', fixed: 'right', width: 320, slots: { default: 'action' } },
],
// 保持数据源
keepSource: true,
// 分页配置
pagerConfig: {},
// 代理配置:用于数据请求
proxyConfig: {
ajax: {
// 请求后端接口方法
// 当表格需要加载数据时,会调用此方法
query: async ({ page }, formValues) => {
// 调用保健食品列表查询API
return await getHealthFoodList({
page: page.currentPage, // 当前页码
pageSize: page.pageSize, // 每页条数
...formValues, // 合并搜索表单的值
page: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
// 表格高度:自动适应
height: 'auto',
// 是否显示边框
border: false,
// 工具栏配置
toolbarConfig: {
// 是否显示搜索表单控制按钮
// @ts-ignore 正式环境时有完整的类型声明
search: true,
refresh: true, // 显示刷新按钮
print: false, // 不显示打印按钮
export: false, // 不显示导出按钮(使用自定义导出按钮)
zoom: true, // 显示最大化最小化按钮
refresh: true,
print: false,
export: false,
zoom: true,
slots: {
buttons: 'toolbar-buttons', // 自定义工具栏按钮插槽
buttons: 'toolbar-buttons',
},
custom: {
// 自定义列-图标
icon: 'vxe-icon-menu',
},
},
// 是否显示溢出内容
showOverflow: false,
};

View File

@@ -9,7 +9,7 @@ import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
// 导入Ant Design Vue组件
import { Button, Image, message, Tag } from 'ant-design-vue';
import { Button, message, Tag } from 'ant-design-vue';
// 导入Vben VxeGrid适配器
import { useVbenVxeGrid } from '#/adapter/vxe-table';
@@ -17,20 +17,27 @@ import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { TableAction } from '#/components/table-action';
// 导入文件下载工具
import { downloadByData } from '#/util/tool';
import CategoryWarehouseCell from '#/views/business/product/_shared/category-warehouse-cell.vue';
import CentralPriceCell from '#/views/business/product/_shared/central-price-cell.vue';
import CentralStockInModal from '#/views/business/product/_shared/central-stock-in-modal.vue';
import ProductInfoCell from '#/views/business/product/_shared/product-info-cell.vue';
import WarehouseDrugPriceModal from '#/views/business/warehouse-drug-management/admin/components/modal.vue';
import BindListModal from '#/views/system/delivery-warehouse-drug/components/bind-list-modal.vue';
import { resolveCentralPrice } from '#/views/system/delivery-warehouse-drug/utils/resolve-central-price';
// 导入保健食品管理相关API
import { deleteHealthFood, exportHealthFoodApi } from './api';
// 导入表单弹窗组件
import FormModalDemo from './components/modal.vue';
// 导入Excel上传组件
import ExcelUpload from './components/ExcelUpload.vue';
// 导入分类设置弹窗组件
import CategoryModal from './components/CategoryModal.vue';
// 导入搜索表单配置
import { formOptions } from './config/search';
// 导入表格配置
import { gridOptions } from './config/table';
/** 保健食品产品类型 */
const PRODUCT_TYPE = 3;
// 控制顶部表格下拉操作按钮的显示状态
const hasTopTableDropDownActions = ref(false);
@@ -74,6 +81,78 @@ const [CategoryModalComp, categoryModalApi] = useVbenModal({
connectedComponent: CategoryModal,
});
const [BindListModalComp, bindListModalApi] = useVbenModal({
connectedComponent: BindListModal,
});
const [CentralStockInModalComp, centralStockInModalApi] = useVbenModal({
connectedComponent: CentralStockInModal,
});
const [WarehousePriceModalComp, warehousePriceModalApi] = useVbenModal({
connectedComponent: WarehouseDrugPriceModal,
});
/**
* 打开某产品的配送仓绑定列表(带总仓售价供费用封顶/默认平台费)
* @param autoCreate 为 true 时打开后立刻弹出新增绑定表单
*/
function openBindList(row: any, autoCreate = false) {
const price = resolveCentralPrice(row);
bindListModalApi.setData({
drug_id: row.id,
drug_name: row.drug_name,
specification: row.specification,
image: row.image,
central_price: price,
values: row,
gridApi,
autoCreate,
});
bindListModalApi.open();
}
/** 列上「新增绑定」:打开列表并自动弹出新增表单 */
function openBindCreate(row: any) {
openBindList(row, true);
}
/** 未入总仓时打开入库弹窗 */
function openStockIn(row: any) {
if (row.in_central_warehouse) {
message.info('该药品已在总仓库');
return;
}
centralStockInModalApi.setData({
drug_id: row.id,
drug_name: row.drug_name,
productType: PRODUCT_TYPE,
values: row,
gridApi,
});
centralStockInModalApi.open();
}
/** 已入库:打开总仓调价弹窗 */
function openCentralPriceAdjust(row: any) {
const cwd = row.central_warehouse_drug;
if (!cwd?.id) {
message.warning('未找到总仓药品记录');
return;
}
warehousePriceModalApi.setData({
update: true,
listDrugType: PRODUCT_TYPE,
gridApi,
values: {
id: cwd.id,
price: cwd.price ?? row.central_price,
market_price: cwd.market_price ?? row.central_market_price,
},
});
warehousePriceModalApi.open();
}
/**
* 打开分类设置弹窗
* @param row - 当前行数据
@@ -155,6 +234,9 @@ const openExcelUploadModal = () => {
<ExcelUploadModal />
<FormModal />
<CategoryModalComp />
<BindListModalComp />
<CentralStockInModalComp />
<WarehousePriceModalComp />
<Grid>
<template #toolbar-buttons>
<TableAction
@@ -199,27 +281,28 @@ const openExcelUploadModal = () => {
</template>
</TableAction>
</template>
<template #image="{ row }">
<Image :src="row.image" height="30" width="30" />
<template #product_info="{ row }">
<ProductInfoCell :row="row" />
</template>
<template #category_warehouse="{ row }">
<CategoryWarehouseCell
:row="row"
@category="openCategoryModal"
@bind="openBindList"
@bind-create="openBindCreate"
@stock-in="openStockIn"
/>
</template>
<template #central_price="{ row }">
<CentralPriceCell
:row="row"
@adjust="openCentralPriceAdjust"
@stock-in="openStockIn"
/>
</template>
<template #supplier="{ row }">
{{ row.supplier?.name || row.source }}
</template>
<template #zone="{ row }">
<a class="text-primary cursor-pointer hover:underline" @click="openCategoryModal(row)">
{{ row.zone_name || '未设置' }}
</a>
</template>
<template #category="{ row }">
<a class="text-primary cursor-pointer hover:underline" @click="openCategoryModal(row)">
{{ row.category_name || '未设置' }}
</a>
</template>
<template #instruction="{ row }">
<div class="div" style="width: 120px; height: 120px; overflow: scroll">
<Image :src="row.instruction" height="30" width="30" />
</div>
</template>
<template #status="{ row }">
<Tag :color="row.status_color">{{ row.status_txt }}</Tag>
</template>
@@ -234,6 +317,13 @@ const openExcelUploadModal = () => {
size: 'small',
onClick: showModal.bind(null, row, true),
},
{
label: '绑定配送仓',
type: 'link',
icon: 'mdi:warehouse',
size: 'small',
onClick: openBindList.bind(null, row),
},
{
label: '删除',
type: 'link',

View File

@@ -14,6 +14,7 @@ interface RowType {
category_name: string;
}
/** 医疗器械表格:合并商品信息/分类仓储/总仓售价列 */
export const gridOptions: VxeGridProps<RowType> = {
checkboxConfig: {
highlight: true,
@@ -27,34 +28,30 @@ export const gridOptions: VxeGridProps<RowType> = {
},
columns: [
{ type: 'checkbox', width: 60 },
{ field: 'id', align: 'left', title: 'ID', width: 100 },
{ field: 'drug_name', align: 'left', title: '器械名称' },
{ field: 'pinyin_simple', title: '拼音' },
{
field: 'drug_name',
align: 'left',
title: '商品信息',
minWidth: 260,
slots: { default: 'product_info' },
},
{
field: 'zone_name',
title: '分',
width: 100,
slots: { default: 'zone' },
title: '分类仓储',
minWidth: 200,
slots: { default: 'category_warehouse' },
},
{
field: 'category_name',
title: '分类',
width: 120,
slots: { default: 'category' },
field: 'central_price',
title: '总仓售价',
width: 110,
slots: { default: 'central_price' },
},
{ field: 'supplier.name', title: '供应商', slots: { default: 'supplier' } },
{
field: 'image',
align: 'left',
title: '产品图片',
slots: { default: 'image' },
width: 130,
},
{ field: 'function', title: '器械功能' },
{ field: 'specification', title: '规格型号' },
{ field: 'status', title: '状态', slots: { default: 'status' } },
{ field: 'created_at', title: '发布时间' },
{ type: 'html', title: '操作', width: 230, slots: { default: 'action' } },
{ field: 'supplier.name', title: '供应商', slots: { default: 'supplier' }, width: 130 },
{ field: 'function', title: '器械功能', width: 200 },
{ field: 'specification', title: '规格型号', width: 120 },
{ field: 'status', title: '状态', width: 90, slots: { default: 'status' } },
{ type: 'html', title: '操作', fixed: 'right', width: 320, slots: { default: 'action' } },
],
keepSource: true,
pagerConfig: {},
@@ -87,11 +84,3 @@ export const gridOptions: VxeGridProps<RowType> = {
},
showOverflow: false,
};

View File

@@ -5,11 +5,18 @@ import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { Button, Image, message, Tag } from 'ant-design-vue';
import { Button, message, Tag } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { TableAction } from '#/components/table-action';
import { downloadByData } from '#/util/tool';
import CategoryWarehouseCell from '#/views/business/product/_shared/category-warehouse-cell.vue';
import CentralPriceCell from '#/views/business/product/_shared/central-price-cell.vue';
import CentralStockInModal from '#/views/business/product/_shared/central-stock-in-modal.vue';
import ProductInfoCell from '#/views/business/product/_shared/product-info-cell.vue';
import WarehouseDrugPriceModal from '#/views/business/warehouse-drug-management/admin/components/modal.vue';
import BindListModal from '#/views/system/delivery-warehouse-drug/components/bind-list-modal.vue';
import { resolveCentralPrice } from '#/views/system/delivery-warehouse-drug/utils/resolve-central-price';
import { deleteMedicalDevice, exportMedicalDeviceApi } from './api';
import FormModalDemo from './components/modal.vue';
@@ -18,6 +25,9 @@ import CategoryModal from './components/CategoryModal.vue';
import { formOptions } from './config/search';
import { gridOptions } from './config/table';
/** 医疗器械产品类型 */
const PRODUCT_TYPE = 7;
const hasTopTableDropDownActions = ref(false);
const gridEvents: VxeGridListeners<any> = {
@@ -49,6 +59,78 @@ const [CategoryModalComp, categoryModalApi] = useVbenModal({
connectedComponent: CategoryModal,
});
const [BindListModalComp, bindListModalApi] = useVbenModal({
connectedComponent: BindListModal,
});
const [CentralStockInModalComp, centralStockInModalApi] = useVbenModal({
connectedComponent: CentralStockInModal,
});
const [WarehousePriceModalComp, warehousePriceModalApi] = useVbenModal({
connectedComponent: WarehouseDrugPriceModal,
});
/**
* 打开某产品的配送仓绑定列表(带总仓售价供费用封顶/默认平台费)
* @param autoCreate 为 true 时打开后立刻弹出新增绑定表单
*/
function openBindList(row: any, autoCreate = false) {
const price = resolveCentralPrice(row);
bindListModalApi.setData({
drug_id: row.id,
drug_name: row.drug_name,
specification: row.specification,
image: row.image,
central_price: price,
values: row,
gridApi,
autoCreate,
});
bindListModalApi.open();
}
/** 列上「新增绑定」:打开列表并自动弹出新增表单 */
function openBindCreate(row: any) {
openBindList(row, true);
}
/** 未入总仓时打开入库弹窗 */
function openStockIn(row: any) {
if (row.in_central_warehouse) {
message.info('该药品已在总仓库');
return;
}
centralStockInModalApi.setData({
drug_id: row.id,
drug_name: row.drug_name,
productType: PRODUCT_TYPE,
values: row,
gridApi,
});
centralStockInModalApi.open();
}
/** 已入库:打开总仓调价弹窗 */
function openCentralPriceAdjust(row: any) {
const cwd = row.central_warehouse_drug;
if (!cwd?.id) {
message.warning('未找到总仓药品记录');
return;
}
warehousePriceModalApi.setData({
update: true,
listDrugType: PRODUCT_TYPE,
gridApi,
values: {
id: cwd.id,
price: cwd.price ?? row.central_price,
market_price: cwd.market_price ?? row.central_market_price,
},
});
warehousePriceModalApi.open();
}
const openCategoryModal = (row: any) => {
categoryModalApi.setData({
row,
@@ -99,6 +181,9 @@ const openExcelUploadModal = () => {
<ExcelUploadModal />
<FormModal />
<CategoryModalComp />
<BindListModalComp />
<CentralStockInModalComp />
<WarehousePriceModalComp />
<Grid>
<template #toolbar-buttons>
<TableAction
@@ -143,22 +228,28 @@ const openExcelUploadModal = () => {
</template>
</TableAction>
</template>
<template #image="{ row }">
<Image :src="row.image" height="30" width="30" />
<template #product_info="{ row }">
<ProductInfoCell :row="row" />
</template>
<template #category_warehouse="{ row }">
<CategoryWarehouseCell
:row="row"
@category="openCategoryModal"
@bind="openBindList"
@bind-create="openBindCreate"
@stock-in="openStockIn"
/>
</template>
<template #central_price="{ row }">
<CentralPriceCell
:row="row"
@adjust="openCentralPriceAdjust"
@stock-in="openStockIn"
/>
</template>
<template #supplier="{ row }">
{{ row.supplier?.name || row.source }}
</template>
<template #zone="{ row }">
<a class="text-primary cursor-pointer hover:underline" @click="openCategoryModal(row)">
{{ row.zone_name || '未设置' }}
</a>
</template>
<template #category="{ row }">
<a class="text-primary cursor-pointer hover:underline" @click="openCategoryModal(row)">
{{ row.category_name || '未设置' }}
</a>
</template>
<template #status="{ row }">
<Tag :color="row.status_color">{{ row.status_txt }}</Tag>
</template>
@@ -173,6 +264,13 @@ const openExcelUploadModal = () => {
size: 'small',
onClick: showModal.bind(null, row, true),
},
{
label: '绑定配送仓',
type: 'link',
icon: 'mdi:warehouse',
size: 'small',
onClick: openBindList.bind(null, row),
},
{
label: '删除',
type: 'link',

View File

@@ -14,6 +14,7 @@ interface RowType {
category_name: string;
}
/** 非药品表格:合并商品信息/分类仓储/总仓售价列 */
export const gridOptions: VxeGridProps<RowType> = {
checkboxConfig: {
highlight: true,
@@ -27,34 +28,30 @@ export const gridOptions: VxeGridProps<RowType> = {
},
columns: [
{ type: 'checkbox', width: 60 },
{ field: 'id', align: 'left', title: 'ID', width: 100 },
{ field: 'drug_name', align: 'left', title: '产品名称' },
{ field: 'pinyin_simple', title: '拼音' },
{
field: 'drug_name',
align: 'left',
title: '商品信息',
minWidth: 260,
slots: { default: 'product_info' },
},
{
field: 'zone_name',
title: '分',
width: 100,
slots: { default: 'zone' },
title: '分类仓储',
minWidth: 200,
slots: { default: 'category_warehouse' },
},
{
field: 'category_name',
title: '分类',
width: 120,
slots: { default: 'category' },
field: 'central_price',
title: '总仓售价',
width: 110,
slots: { default: 'central_price' },
},
{ field: 'supplier.name', title: '供应商', slots: { default: 'supplier' } },
{
field: 'image',
align: 'left',
title: '产品图片',
slots: { default: 'image' },
width: 130,
},
{ field: 'function', title: '产品功能' },
{ field: 'specification', title: '规格' },
{ field: 'status', title: '状态', slots: { default: 'status' } },
{ field: 'created_at', title: '发布时间' },
{ type: 'html', title: '操作', width: 230, slots: { default: 'action' } },
{ field: 'supplier.name', title: '供应商', slots: { default: 'supplier' }, width: 130 },
{ field: 'function', title: '产品功能', width: 200 },
{ field: 'specification', title: '规格', width: 120 },
{ field: 'status', title: '状态', width: 90, slots: { default: 'status' } },
{ type: 'html', title: '操作', fixed: 'right', width: 320, slots: { default: 'action' } },
],
keepSource: true,
pagerConfig: {},
@@ -87,11 +84,3 @@ export const gridOptions: VxeGridProps<RowType> = {
},
showOverflow: false,
};

View File

@@ -5,11 +5,18 @@ import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { Button, Image, message, Tag } from 'ant-design-vue';
import { Button, message, Tag } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { TableAction } from '#/components/table-action';
import { downloadByData } from '#/util/tool';
import CategoryWarehouseCell from '#/views/business/product/_shared/category-warehouse-cell.vue';
import CentralPriceCell from '#/views/business/product/_shared/central-price-cell.vue';
import CentralStockInModal from '#/views/business/product/_shared/central-stock-in-modal.vue';
import ProductInfoCell from '#/views/business/product/_shared/product-info-cell.vue';
import WarehouseDrugPriceModal from '#/views/business/warehouse-drug-management/admin/components/modal.vue';
import BindListModal from '#/views/system/delivery-warehouse-drug/components/bind-list-modal.vue';
import { resolveCentralPrice } from '#/views/system/delivery-warehouse-drug/utils/resolve-central-price';
import { deleteNonDrug, exportNonDrugApi } from './api';
import FormModalDemo from './components/modal.vue';
@@ -18,6 +25,9 @@ import CategoryModal from './components/CategoryModal.vue';
import { formOptions } from './config/search';
import { gridOptions } from './config/table';
/** 非药品产品类型 */
const PRODUCT_TYPE = 6;
const hasTopTableDropDownActions = ref(false);
const gridEvents: VxeGridListeners<any> = {
@@ -49,6 +59,78 @@ const [CategoryModalComp, categoryModalApi] = useVbenModal({
connectedComponent: CategoryModal,
});
const [BindListModalComp, bindListModalApi] = useVbenModal({
connectedComponent: BindListModal,
});
const [CentralStockInModalComp, centralStockInModalApi] = useVbenModal({
connectedComponent: CentralStockInModal,
});
const [WarehousePriceModalComp, warehousePriceModalApi] = useVbenModal({
connectedComponent: WarehouseDrugPriceModal,
});
/**
* 打开某产品的配送仓绑定列表(带总仓售价供费用封顶/默认平台费)
* @param autoCreate 为 true 时打开后立刻弹出新增绑定表单
*/
function openBindList(row: any, autoCreate = false) {
const price = resolveCentralPrice(row);
bindListModalApi.setData({
drug_id: row.id,
drug_name: row.drug_name,
specification: row.specification,
image: row.image,
central_price: price,
values: row,
gridApi,
autoCreate,
});
bindListModalApi.open();
}
/** 列上「新增绑定」:打开列表并自动弹出新增表单 */
function openBindCreate(row: any) {
openBindList(row, true);
}
/** 未入总仓时打开入库弹窗 */
function openStockIn(row: any) {
if (row.in_central_warehouse) {
message.info('该药品已在总仓库');
return;
}
centralStockInModalApi.setData({
drug_id: row.id,
drug_name: row.drug_name,
productType: PRODUCT_TYPE,
values: row,
gridApi,
});
centralStockInModalApi.open();
}
/** 已入库:打开总仓调价弹窗 */
function openCentralPriceAdjust(row: any) {
const cwd = row.central_warehouse_drug;
if (!cwd?.id) {
message.warning('未找到总仓药品记录');
return;
}
warehousePriceModalApi.setData({
update: true,
listDrugType: PRODUCT_TYPE,
gridApi,
values: {
id: cwd.id,
price: cwd.price ?? row.central_price,
market_price: cwd.market_price ?? row.central_market_price,
},
});
warehousePriceModalApi.open();
}
const openCategoryModal = (row: any) => {
categoryModalApi.setData({
row,
@@ -99,6 +181,9 @@ const openExcelUploadModal = () => {
<ExcelUploadModal />
<FormModal />
<CategoryModalComp />
<BindListModalComp />
<CentralStockInModalComp />
<WarehousePriceModalComp />
<Grid>
<template #toolbar-buttons>
<TableAction
@@ -143,22 +228,28 @@ const openExcelUploadModal = () => {
</template>
</TableAction>
</template>
<template #image="{ row }">
<Image :src="row.image" height="30" width="30" />
<template #product_info="{ row }">
<ProductInfoCell :row="row" />
</template>
<template #category_warehouse="{ row }">
<CategoryWarehouseCell
:row="row"
@category="openCategoryModal"
@bind="openBindList"
@bind-create="openBindCreate"
@stock-in="openStockIn"
/>
</template>
<template #central_price="{ row }">
<CentralPriceCell
:row="row"
@adjust="openCentralPriceAdjust"
@stock-in="openStockIn"
/>
</template>
<template #supplier="{ row }">
{{ row.supplier?.name || row.source }}
</template>
<template #zone="{ row }">
<a class="text-primary cursor-pointer hover:underline" @click="openCategoryModal(row)">
{{ row.zone_name || '未设置' }}
</a>
</template>
<template #category="{ row }">
<a class="text-primary cursor-pointer hover:underline" @click="openCategoryModal(row)">
{{ row.category_name || '未设置' }}
</a>
</template>
<template #status="{ row }">
<Tag :color="row.status_color">{{ row.status_txt }}</Tag>
</template>
@@ -173,6 +264,13 @@ const openExcelUploadModal = () => {
size: 'small',
onClick: showModal.bind(null, row, true),
},
{
label: '绑定配送仓',
type: 'link',
icon: 'mdi:warehouse',
size: 'small',
onClick: openBindList.bind(null, row),
},
{
label: '删除',
type: 'link',

View File

@@ -14,6 +14,7 @@ interface RowType {
category_name: string;
}
/** 产品服务包表格:合并商品信息/分类仓储/总仓售价列 */
export const gridOptions: VxeGridProps<RowType> = {
checkboxConfig: {
highlight: true,
@@ -27,47 +28,35 @@ export const gridOptions: VxeGridProps<RowType> = {
},
columns: [
{ type: 'checkbox', width: 60 },
{ field: 'id', align: 'left', title: 'ID', width: 100 },
{ field: 'drug_name', align: 'left', title: '服务包名称' },
{ field: 'pinyin_simple', title: '拼音' },
{
field: 'drug_name',
align: 'left',
title: '商品信息',
minWidth: 260,
slots: { default: 'product_info' },
},
{
field: 'zone_name',
title: '分',
width: 100,
slots: { default: 'zone' },
title: '分类仓储',
minWidth: 200,
slots: { default: 'category_warehouse' },
},
{
field: 'category_name',
title: '分类',
width: 120,
slots: { default: 'category' },
field: 'central_price',
title: '总仓售价',
width: 110,
slots: { default: 'central_price' },
},
{ field: 'supplier.name', title: '供应商' },
{
field: 'image',
align: 'left',
title: '产品图片',
slots: { default: 'image' },
width: 130,
},
{
field: 'instruction',
align: 'left',
title: '说明书',
slots: { default: 'instruction' },
width: 130,
},
{ field: 'function', title: '主要功能' },
{ field: 'specification', title: '规格' },
{ field: 'status', title: '状态', slots: { default: 'status' } },
{ field: 'created_at', title: '发布时间' },
{ type: 'html', title: '操作', slots: { default: 'action' } },
{ field: 'supplier.name', title: '供应商', width: 130 },
{ field: 'function', title: '主要功能', width: 200 },
{ field: 'specification', title: '规格', width: 120 },
{ field: 'status', title: '状态', width: 90, slots: { default: 'status' } },
{ type: 'html', title: '操作', fixed: 'right', width: 320, slots: { default: 'action' } },
],
keepSource: true,
pagerConfig: {},
proxyConfig: {
ajax: {
// 请求后端接口方法
query: async ({ page }, formValues) => {
return await getServicePackList({
page: page.currentPage,
@@ -80,19 +69,16 @@ export const gridOptions: VxeGridProps<RowType> = {
height: 'auto',
border: false,
toolbarConfig: {
// 是否显示搜索表单控制按钮
// @ts-ignore 正式环境时有完整的类型声明
search: true,
refresh: true, // 刷新
print: false, // 打印
export: false, // 导出
// custom: true, // 自定义列
zoom: true, // 最大化最小化
refresh: true,
print: false,
export: false,
zoom: true,
slots: {
buttons: 'toolbar-buttons',
},
custom: {
// 自定义列-图标
icon: 'vxe-icon-menu',
},
},

View File

@@ -5,10 +5,17 @@ import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { Button, Image, message, Tag } from 'ant-design-vue';
import { Button, message, Tag } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { TableAction } from '#/components/table-action';
import CategoryWarehouseCell from '#/views/business/product/_shared/category-warehouse-cell.vue';
import CentralPriceCell from '#/views/business/product/_shared/central-price-cell.vue';
import CentralStockInModal from '#/views/business/product/_shared/central-stock-in-modal.vue';
import ProductInfoCell from '#/views/business/product/_shared/product-info-cell.vue';
import WarehouseDrugPriceModal from '#/views/business/warehouse-drug-management/admin/components/modal.vue';
import BindListModal from '#/views/system/delivery-warehouse-drug/components/bind-list-modal.vue';
import { resolveCentralPrice } from '#/views/system/delivery-warehouse-drug/utils/resolve-central-price';
import { deleteServicePack } from './api';
import FormModalDemo from './components/modal.vue';
@@ -16,6 +23,9 @@ import ZoneCategoryModal from './components/ZoneCategoryModal.vue';
import { formOptions } from './config/search';
import { gridOptions } from './config/table';
/** 产品服务包产品类型 */
const PRODUCT_TYPE = 5;
const hasTopTableDropDownActions = ref(false);
const gridEvents: VxeGridListeners<any> = {
@@ -45,6 +55,79 @@ const [ZoneCategoryModalComp, zoneCategoryModalApi] = useVbenModal({
connectedComponent: ZoneCategoryModal,
});
const [BindListModalComp, bindListModalApi] = useVbenModal({
connectedComponent: BindListModal,
});
const [CentralStockInModalComp, centralStockInModalApi] = useVbenModal({
connectedComponent: CentralStockInModal,
});
const [WarehousePriceModalComp, warehousePriceModalApi] = useVbenModal({
connectedComponent: WarehouseDrugPriceModal,
});
/**
* 打开某产品的配送仓绑定列表(带总仓售价供费用封顶/默认平台费)
* @param autoCreate 为 true 时打开后立刻弹出新增绑定表单
*/
function openBindList(row: any, autoCreate = false) {
const price = resolveCentralPrice(row);
bindListModalApi.setData({
drug_id: row.id,
drug_name: row.drug_name,
specification: row.specification,
image: row.image,
central_price: price,
values: row,
gridApi,
autoCreate,
});
bindListModalApi.open();
}
/** 列上「新增绑定」:打开列表并自动弹出新增表单 */
function openBindCreate(row: any) {
openBindList(row, true);
}
/** 未入总仓时打开入库弹窗 */
function openStockIn(row: any) {
if (row.in_central_warehouse) {
message.info('该药品已在总仓库');
return;
}
centralStockInModalApi.setData({
drug_id: row.id,
drug_name: row.drug_name,
productType: PRODUCT_TYPE,
values: row,
gridApi,
});
centralStockInModalApi.open();
}
/** 已入库:打开总仓调价弹窗 */
function openCentralPriceAdjust(row: any) {
const cwd = row.central_warehouse_drug;
if (!cwd?.id) {
message.warning('未找到总仓药品记录');
return;
}
warehousePriceModalApi.setData({
update: true,
listDrugType: PRODUCT_TYPE,
gridApi,
values: {
id: cwd.id,
price: cwd.price ?? row.central_price,
market_price: cwd.market_price ?? row.central_market_price,
},
});
warehousePriceModalApi.open();
}
/** 打开服务包分区/分类设置弹窗ZoneCategoryModal */
const openZoneCategoryModal = (row: any) => {
zoneCategoryModalApi.setData({
row,
@@ -81,6 +164,9 @@ const deleteApi = (row: any) => {
<Page auto-content-height title="产品服务包管理">
<FormModal />
<ZoneCategoryModalComp />
<BindListModalComp />
<CentralStockInModalComp />
<WarehousePriceModalComp />
<Grid>
<template #toolbar-buttons>
<TableAction
@@ -114,23 +200,24 @@ const deleteApi = (row: any) => {
</template>
</TableAction>
</template>
<template #zone="{ row }">
<a class="text-primary cursor-pointer hover:underline" @click="openZoneCategoryModal(row)">
{{ row.zone_name || '未设置' }}
</a>
<template #product_info="{ row }">
<ProductInfoCell :row="row" />
</template>
<template #category="{ row }">
<a class="text-primary cursor-pointer hover:underline" @click="openZoneCategoryModal(row)">
{{ row.category_name || '未设置' }}
</a>
<template #category_warehouse="{ row }">
<CategoryWarehouseCell
:row="row"
@category="openZoneCategoryModal"
@bind="openBindList"
@bind-create="openBindCreate"
@stock-in="openStockIn"
/>
</template>
<template #image="{ row }">
<Image :src="row.image" height="30" width="30" />
</template>
<template #instruction="{ row }">
<div class="div" style="width: 120px; height: 120px; overflow: hidden">
<Image :src="row.instruction" height="30" width="30" />
</div>
<template #central_price="{ row }">
<CentralPriceCell
:row="row"
@adjust="openCentralPriceAdjust"
@stock-in="openStockIn"
/>
</template>
<template #status="{ row }">
<Tag :color="row.status_color">{{ row.status_txt }}</Tag>
@@ -147,6 +234,13 @@ const deleteApi = (row: any) => {
// auth: ['service-pack', 'sys:role:detail'],
onClick: showModal.bind(null, row, true),
},
{
label: '绑定配送仓',
type: 'link',
icon: 'mdi:warehouse',
size: 'small',
onClick: openBindList.bind(null, row),
},
{
label: '删除',
type: 'link',

View File

@@ -27,60 +27,53 @@ export const gridOptions: VxeGridProps<RowType> = {
},
columns: [
{ type: 'checkbox', width: 60 },
{ field: 'id', align: 'left', title: 'ID', width: 100 },
{ field: 'drug_name', align: 'left', title: '药品名称', width: 120 },
{ field: 'pinyin_simple', title: '拼音', width: 120 },
{
field: 'drug_name',
align: 'left',
title: '商品信息',
minWidth: 260,
slots: { default: 'product_info' },
},
{
field: 'zone_name',
title: '分',
width: 100,
slots: { default: 'zone' },
title: '分类仓储',
minWidth: 200,
slots: { default: 'category_warehouse' },
},
{
field: 'category_name',
title: '分类',
width: 120,
slots: { default: 'category' },
field: 'central_price',
title: '总仓售价',
width: 110,
slots: { default: 'central_price' },
},
{ field: 'supplier.name', title: '供应商', slots: { default: 'supplier' },width: 130 },
{
field: 'image',
align: 'left',
title: '产品图片',
slots: { default: 'image' },
width: 130,
},
{
field: 'instruction',
align: 'left',
title: '说明书',
slots: { default: 'instruction' },
width: 130,
},
{ field: 'function', title: '主要功能', width: 240 },
{ field: 'specification', title: '规格', width: 130 },
{ field: 'supplier.name', title: '供应商', slots: { default: 'supplier' }, width: 130 },
{ field: 'function', title: '主要功能', width: 200 },
{ field: 'specification', title: '规格', width: 120 },
{
field: 'is_otc',
title: '处方药',
width: 100,
slots: { default: 'is_otc' },
},
{ field: 'status', title: '状态', slots: { default: 'status' } },
{ field: 'created_at', title: '发布时间', width: 240 },
{ field: 'status', title: '状态', width: 90, slots: { default: 'status' } },
{
field: 'min_adjust_price',
title: '最低调整金额',
width: 130,
width: 120,
slots: { default: 'min_adjust_price' },
},
{ field: 'erp_qty_factor', title: 'ERP抓取数量倍数', width: 150, slots: { default: 'erp_qty_factor' } },
{ type: 'html', title: '操作', fixed: 'right', width: 280, slots: { default: 'action' } },
{
field: 'erp_qty_factor',
title: 'ERP抓取数量倍数',
width: 140,
slots: { default: 'erp_qty_factor' },
},
{ type: 'html', title: '操作', fixed: 'right', width: 340, slots: { default: 'action' } },
],
keepSource: true,
pagerConfig: {},
proxyConfig: {
ajax: {
// 请求后端接口方法
query: async ({ page }, formValues) => {
return await getWesternMedicineList({
page: page.currentPage,
@@ -90,25 +83,19 @@ export const gridOptions: VxeGridProps<RowType> = {
},
},
},
// exportConfig: {
// api: passApplicationApi,
// },
height: 'auto',
border: false,
toolbarConfig: {
// 是否显示搜索表单控制按钮
// @ts-ignore 正式环境时有完整的类型声明
// @ts-ignore
search: true,
refresh: true, // 刷新
print: false, // 打印
export: false, // 导出
// custom: true, // 自定义列
zoom: true, // 最大化最小化
refresh: true,
print: false,
export: false,
zoom: true,
slots: {
buttons: 'toolbar-buttons',
},
custom: {
// 自定义列-图标
icon: 'vxe-icon-menu',
},
},

View File

@@ -5,11 +5,18 @@ import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { Button, Image, message, Tag } from 'ant-design-vue';
import { Button, message, Tag } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { TableAction } from '#/components/table-action';
import { downloadByData } from '#/util/tool';
import CategoryWarehouseCell from '#/views/business/product/_shared/category-warehouse-cell.vue';
import CentralPriceCell from '#/views/business/product/_shared/central-price-cell.vue';
import CentralStockInModal from '#/views/business/product/_shared/central-stock-in-modal.vue';
import ProductInfoCell from '#/views/business/product/_shared/product-info-cell.vue';
import WarehouseDrugPriceModal from '#/views/business/warehouse-drug-management/admin/components/modal.vue';
import BindListModal from '#/views/system/delivery-warehouse-drug/components/bind-list-modal.vue';
import { resolveCentralPrice } from '#/views/system/delivery-warehouse-drug/utils/resolve-central-price';
import { deleteWesternMedicine, exportWesternMedicineApi } from './api';
import FormModalDemo from './components/modal.vue';
@@ -20,6 +27,9 @@ import ErpQtyFactorModal from './components/ErpQtyFactorModal.vue';
import { formOptions } from './config/search';
import { gridOptions } from './config/table';
/** 西药(含中成药列表)产品类型 */
const PRODUCT_TYPE = 2;
const hasTopTableDropDownActions = ref(false);
const gridEvents: VxeGridListeners<any> = {
@@ -61,6 +71,78 @@ const [ErpQtyFactorModalComp, erpQtyFactorModalApi] = useVbenModal({
connectedComponent: ErpQtyFactorModal,
});
const [BindListModalComp, bindListModalApi] = useVbenModal({
connectedComponent: BindListModal,
});
const [CentralStockInModalComp, centralStockInModalApi] = useVbenModal({
connectedComponent: CentralStockInModal,
});
const [WarehousePriceModalComp, warehousePriceModalApi] = useVbenModal({
connectedComponent: WarehouseDrugPriceModal,
});
/**
* 打开某药的配送仓绑定列表(带总仓售价供费用封顶/默认平台费)
* @param autoCreate 为 true 时打开后立刻弹出新增绑定表单
*/
function openBindList(row: any, autoCreate = false) {
const price = resolveCentralPrice(row);
bindListModalApi.setData({
drug_id: row.id,
drug_name: row.drug_name,
specification: row.specification,
image: row.image,
central_price: price,
values: row,
gridApi,
autoCreate,
});
bindListModalApi.open();
}
/** 列上「新增绑定」:打开列表并自动弹出新增表单 */
function openBindCreate(row: any) {
openBindList(row, true);
}
/** 未入总仓时打开入库弹窗 */
function openStockIn(row: any) {
if (row.in_central_warehouse) {
message.info('该药品已在总仓库');
return;
}
centralStockInModalApi.setData({
drug_id: row.id,
drug_name: row.drug_name,
productType: PRODUCT_TYPE,
values: row,
gridApi,
});
centralStockInModalApi.open();
}
/** 已入库:打开总仓调价弹窗 */
function openCentralPriceAdjust(row: any) {
const cwd = row.central_warehouse_drug;
if (!cwd?.id) {
message.warning('未找到总仓药品记录');
return;
}
warehousePriceModalApi.setData({
update: true,
listDrugType: PRODUCT_TYPE,
gridApi,
values: {
id: cwd.id,
price: cwd.price ?? row.central_price,
market_price: cwd.market_price ?? row.central_market_price,
},
});
warehousePriceModalApi.open();
}
const openCategoryModal = (row: any) => {
categoryModalApi.setData({
row,
@@ -138,6 +220,9 @@ const openExcelUploadModal = () => {
<CategoryModalComp />
<MinPriceModalComp />
<ErpQtyFactorModalComp />
<BindListModalComp />
<CentralStockInModalComp />
<WarehousePriceModalComp />
<Grid>
<template #toolbar-buttons>
<TableAction
@@ -186,27 +271,28 @@ const openExcelUploadModal = () => {
</template>
</TableAction>
</template>
<template #image="{ row }">
<Image :src="row.image" height="30" width="30" />
<template #product_info="{ row }">
<ProductInfoCell :row="row" />
</template>
<template #category_warehouse="{ row }">
<CategoryWarehouseCell
:row="row"
@category="openCategoryModal"
@bind="openBindList"
@bind-create="openBindCreate"
@stock-in="openStockIn"
/>
</template>
<template #central_price="{ row }">
<CentralPriceCell
:row="row"
@adjust="openCentralPriceAdjust"
@stock-in="openStockIn"
/>
</template>
<template #supplier="{ row }">
{{ row.supplier?.name || row.source }}
</template>
<template #zone="{ row }">
<a class="text-primary cursor-pointer hover:underline" @click="openCategoryModal(row)">
{{ row.zone_name || '未设置' }}
</a>
</template>
<template #category="{ row }">
<a class="text-primary cursor-pointer hover:underline" @click="openCategoryModal(row)">
{{ row.category_name || '未设置' }}
</a>
</template>
<template #instruction="{ row }">
<div class="div" style="width: 120px; height: 120px; overflow: scroll">
<Image :src="row.instruction" height="30" width="30" />
</div>
</template>
<template #is_otc="{ row }">
<Tag :color="row.is_otc === 0 ? 'red' : 'green'">
{{ row.is_otc === 0 ? '处方药' : '非处方药' }}
@@ -246,6 +332,13 @@ const openExcelUploadModal = () => {
// auth: ['western-medicine', 'sys:role:detail'],
onClick: showModal.bind(null, row, true),
},
{
label: '绑定配送仓',
type: 'link',
icon: 'mdi:warehouse',
size: 'small',
onClick: openBindList.bind(null, row),
},
{
label: '设置底价',
type: 'link',

View File

@@ -2,6 +2,18 @@ import { requestClient } from '#/api/request';
const prefix = 'doctor-reception/';
/**
* 按药品查询可选配送仓库(药店开方选仓)
*/
export async function getDeliveryWarehouseOptionsByDrugs(data: {
drug_ids: number[] | string;
need_qty_map?: Record<number | string, number>;
}) {
return requestClient.get<any>('delivery-warehouse-drug/options-by-drugs', {
params: data,
});
}
/**
* 医生应用特色方到开方区
*/

View File

@@ -5,6 +5,7 @@ const UNUSED_ID_FIELDS = [
'doctor_id',
'pharmacist_id',
'supplier_id',
'delivery_warehouse_id',
] as const;
function isEmptyId(value: unknown): boolean {
@@ -53,15 +54,24 @@ export function normalizeAdminPayload(
toZeroOrOmit(payload, 'store_id', true);
toZeroOrOmit(payload, 'doctor_id', true);
toZeroOrOmit(payload, 'pharmacist_id', true);
toZeroOrOmit(payload, 'delivery_warehouse_id', true);
break;
case 'deliveryWarehouse':
toZeroOrOmit(payload, 'store_id', true);
toZeroOrOmit(payload, 'doctor_id', true);
toZeroOrOmit(payload, 'pharmacist_id', true);
toZeroOrOmit(payload, 'supplier_id', true);
break;
case 'clinic':
toZeroOrOmit(payload, 'doctor_id', true);
toZeroOrOmit(payload, 'pharmacist_id', true);
toZeroOrOmit(payload, 'supplier_id', true);
toZeroOrOmit(payload, 'delivery_warehouse_id', true);
break;
case 'doctor':
toZeroOrOmit(payload, 'pharmacist_id', true);
toZeroOrOmit(payload, 'supplier_id', true);
toZeroOrOmit(payload, 'delivery_warehouse_id', true);
if (isEmptyId(payload.doctor_id)) {
payload.doctor_id = 0;
}
@@ -69,6 +79,7 @@ export function normalizeAdminPayload(
case 'pharmacist':
toZeroOrOmit(payload, 'doctor_id', true);
toZeroOrOmit(payload, 'supplier_id', true);
toZeroOrOmit(payload, 'delivery_warehouse_id', true);
if (isEmptyId(payload.pharmacist_id)) {
payload.pharmacist_id = 0;
}

View File

@@ -3,6 +3,7 @@ import type { VbenFormProps } from '#/adapter/form';
import { useUserStore } from '@vben/stores';
import { z } from '#/adapter/form';
import { getDeliveryWarehouseOption } from '#/views/system/delivery-warehouse/api';
import { getSupplierOption } from '#/views/system/supplier/api';
import type { AdminFormType } from './role-meta';
@@ -177,6 +178,29 @@ export function createAdminModalFormProps(
});
}
if (formType === 'deliveryWarehouse') {
schema.push({
component: 'ApiSelect',
componentProps: {
allowClear: true,
filterOption: true,
showSearch: true,
afterFetch: (data: { id: number; name: string }[]) => {
return data.map((item) => ({
label: item.name,
value: item.id,
}));
},
api: getDeliveryWarehouseOption,
placeholder: '请选择',
},
fieldName: 'delivery_warehouse_id',
formItemClass: 'col-span-6',
label: '所属配送仓库',
rules: 'required',
});
}
if (formType === 'clinic' || formType === 'doctor' || formType === 'pharmacist') {
schema.push({
component: 'StorePicker',

View File

@@ -2,6 +2,7 @@ export type AdminFormType =
| 'basic'
| 'address'
| 'supplier'
| 'deliveryWarehouse'
| 'clinic'
| 'doctor'
| 'pharmacist';
@@ -28,6 +29,8 @@ export const ADMIN_ROLE_LIST: AdminRoleMeta[] = [
{ id: 10, title: '医生', slug: 'doctor', formType: 'doctor' },
{ id: 11, title: '药师', slug: 'pharmacist', formType: 'pharmacist' },
{ id: 14, title: '诊所推广员', slug: 'clinic-salesperson', formType: 'clinic' },
{ id: 15, title: '订单管理员', slug: 'order', formType: 'basic' },
{ id: 16, title: '配送仓库管理员', slug: 'delivery-warehouse', formType: 'deliveryWarehouse' },
];
export const ROLE_CITY_MANAGER = 4;

View File

@@ -82,6 +82,10 @@ function buildColumns(formType: AdminFormType, roleId: number) {
cols.push({ field: 'supplier.name', title: '所属供应商' });
}
if (formType === 'deliveryWarehouse') {
cols.push({ field: 'delivery_warehouse.name', title: '所属配送仓库' });
}
if (formType === 'clinic' || formType === 'doctor' || formType === 'pharmacist') {
cols.push({
field: 'store_id',

View File

@@ -2,11 +2,13 @@ import type { VbenFormProps } from '#/adapter/form';
import { z } from '#/adapter/form';
import { getRoleOption } from '#/views/system/role/api';
import { getDeliveryWarehouseOption } from '#/views/system/delivery-warehouse/api';
import { getSupplierOption } from '#/views/system/supplier/api';
const defaultPassword = 'Xk123456@';
const supplierId = 7;
const deliveryWarehouseAdminId = 16;
const pharmacistId = 3;
const cityId = 4;
const districtId = 5;
@@ -123,6 +125,32 @@ export const modalFormProps: VbenFormProps = {
label: '所属供应商',
rules: 'required',
},
{
component: 'ApiSelect',
componentProps: {
allowClear: true,
filterOption: true,
showSearch: true,
afterFetch: (data: { id: number; name: string }[]) => {
return data.map((item: any) => ({
label: item.name,
value: item.id,
}));
},
api: getDeliveryWarehouseOption,
placeholder: '请选择',
},
dependencies: {
show: (values) => {
return values.role_id === deliveryWarehouseAdminId;
},
triggerFields: ['role_id'],
},
fieldName: 'delivery_warehouse_id',
formItemClass: 'col-span-6',
label: '所属配送仓库',
rules: 'required',
},
{
fieldName: 'password',
label: '密码',

View File

@@ -0,0 +1,9 @@
<script lang="ts" setup>
/**
* 配送仓管理员账号列表(角色 16
*/
import AdminPage from '../_shared/admin-page.vue';
</script>
<template>
<AdminPage :role-id="16" />
</template>

View File

@@ -0,0 +1,10 @@
<script lang="ts" setup>
/**
* 订单管理员账号列表(角色 15
* 本阶段仅账号入口,业务菜单权限后续再挂
*/
import AdminPage from '../_shared/admin-page.vue';
</script>
<template>
<AdminPage :role-id="15" />
</template>

View File

@@ -0,0 +1,26 @@
import { requestClient } from '#/api/request';
const prefix = 'delivery-warehouse-bank-card-report/';
/**
* 获取银行卡报备详情
*/
export async function getDeliveryWarehouseBankCardDetail(params?: {
warehouse_id?: number;
}) {
return requestClient.get<any>(`${prefix}detail`, { params });
}
/**
* 提交银行卡报备
*/
export async function reportDeliveryWarehouseBankCard(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}report`, data);
}
/**
* 同步报备状态
*/
export async function syncDeliveryWarehouseBankCardStatus(data?: Record<string, any>) {
return requestClient.post<any>(`${prefix}sync-status`, data || {});
}

View File

@@ -0,0 +1,98 @@
import type { VbenFormProps } from '#/adapter/form';
export const reportFormProps: VbenFormProps = {
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: 'warehouse_id',
label: '仓库ID',
dependencies: {
show: false,
triggerFields: ['warehouse_id'],
},
},
{
component: 'VbenInput',
componentProps: {
placeholder: '请输入开户人姓名',
},
fieldName: 'bank_user_name',
label: '开户人姓名',
rules: 'required',
},
{
component: 'VbenInput',
componentProps: {
placeholder: '请输入银行卡号',
},
fieldName: 'bank_card',
label: '银行卡号',
rules: 'required',
},
{
component: 'VbenInput',
componentProps: {
placeholder: '请输入开户行',
},
fieldName: 'bank_name',
label: '开户行',
rules: 'required',
},
{
component: 'RadioGroup',
componentProps: {
options: [
{ label: '对公', value: 1 },
{ label: '对私', value: 2 },
{ label: '存折', value: 5 },
],
},
fieldName: 'bank_account_type',
label: '账户类型',
defaultValue: 2,
},
{
component: 'VbenInput',
componentProps: {
placeholder: '请输入联行号',
},
fieldName: 'bank_no',
label: '联行号',
},
{
component: 'VbenInput',
componentProps: {
placeholder: '请输入证件号码',
},
fieldName: 'cert_no',
label: '证件号码',
},
{
component: 'UploadOssFile',
componentProps: {
maxCount: 1,
},
fieldName: 'attachment',
label: '合同附件',
rules: 'required',
},
],
showDefaultActions: false,
};

View File

@@ -0,0 +1,145 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { Page } from '@vben/common-ui';
import { Button, Card, Descriptions, message, Tag } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
getDeliveryWarehouseBankCardDetail,
reportDeliveryWarehouseBankCard,
syncDeliveryWarehouseBankCardStatus,
} from './api';
import { reportFormProps } from './config/form';
const loading = ref(false);
const syncing = ref(false);
const detail = ref<Record<string, any>>({});
const reportStatusMap: Record<number, { text: string; color: string }> = {
1: { text: '待审核', color: 'orange' },
2: { text: '有效', color: 'green' },
3: { text: '审核不通过', color: 'red' },
4: { text: '无效', color: 'default' },
};
const bankAccountTypeMap: Record<number, string> = {
1: '对公',
2: '对私',
5: '存折',
};
const [Form, formApi] = useVbenForm(reportFormProps);
async function loadDetail() {
loading.value = true;
try {
const res = await getDeliveryWarehouseBankCardDetail();
detail.value = res || {};
if (res) {
formApi.setValues({
id: res.id,
warehouse_id: res.warehouse_id,
bank_user_name: res.bank_user_name,
bank_card: res.bank_card,
bank_name: res.bank_name,
bank_account_type: res.bank_account_type,
bank_no: res.bank_no,
cert_no: res.cert_no,
attachment: res.attachment,
});
}
} finally {
loading.value = false;
}
}
async function handleSubmit() {
const result = await formApi.validate();
if (!result.valid) {
return;
}
const values = await formApi.getValues();
loading.value = true;
try {
await reportDeliveryWarehouseBankCard(values);
message.success('报备提交成功');
await loadDetail();
} finally {
loading.value = false;
}
}
async function handleSyncStatus() {
syncing.value = true;
try {
await syncDeliveryWarehouseBankCardStatus(
detail.value.warehouse_id
? { warehouse_id: detail.value.warehouse_id }
: {},
);
message.success('状态已同步');
await loadDetail();
} finally {
syncing.value = false;
}
}
onMounted(() => {
loadDetail();
});
</script>
<template>
<Page auto-content-height title="银行卡报备">
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
<Card :loading="loading" title="当前报备状态">
<Descriptions :column="1" bordered size="small">
<Descriptions.Item label="报备状态">
<Tag
v-if="detail.report_status"
:color="reportStatusMap[detail.report_status]?.color"
>
{{
detail.report_status_text ||
reportStatusMap[detail.report_status]?.text ||
'-'
}}
</Tag>
<span v-else>未报备</span>
</Descriptions.Item>
<Descriptions.Item label="审核意见">
{{ detail.audit_remarks || detail.report_status_msg || '-' }}
</Descriptions.Item>
<Descriptions.Item label="开户人">
{{ detail.bank_user_name || '-' }}
</Descriptions.Item>
<Descriptions.Item label="银行卡号">
{{ detail.bank_card || '-' }}
</Descriptions.Item>
<Descriptions.Item label="开户行">
{{ detail.bank_name || '-' }}
</Descriptions.Item>
<Descriptions.Item label="账户类型">
{{ bankAccountTypeMap[detail.bank_account_type] || '-' }}
</Descriptions.Item>
</Descriptions>
<div class="mt-4">
<Button :loading="syncing" type="primary" @click="handleSyncStatus">
查询状态
</Button>
</div>
</Card>
<Card :loading="loading" title="提交报备">
<Form />
<div class="mt-4">
<Button :loading="loading" type="primary" @click="handleSubmit">
提交报备
</Button>
</div>
</Card>
</div>
</Page>
</template>

View File

@@ -0,0 +1,71 @@
import { requestClient } from '#/api/request';
const prefix = 'delivery-warehouse-drug/';
/**
* 分页查询仓药绑定列表
*/
export async function getDeliveryWarehouseDrugList(data: any) {
return requestClient.get<any>(`${prefix}list`, { params: data });
}
/**
* 获取仓药绑定详情
*/
export async function getDeliveryWarehouseDrugInfo(id: number) {
return requestClient.get<any>(`${prefix}detail`, { params: { id } });
}
/**
* 新增仓药绑定
*/
export async function createDeliveryWarehouseDrug(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}create`, data);
}
/**
* 编辑仓药绑定
*/
export async function updateDeliveryWarehouseDrug(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}update`, data);
}
/**
* 删除仓药绑定
*/
export async function deleteDeliveryWarehouseDrug(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}delete`, data);
}
/**
* @deprecated 已禁止直改库存
*/
export async function updateDeliveryWarehouseDrugStock(data: {
id: number;
stock: number;
}) {
return requestClient.post<any>(`${prefix}update-stock`, data);
}
/** 手动入库 */
export async function stockInDeliveryWarehouseDrug(data: {
id: number;
qty: number;
remark?: string;
}) {
return requestClient.post<any>(`${prefix}stock-in`, data);
}
/** 手动出库 */
export async function stockOutDeliveryWarehouseDrug(data: {
id: number;
qty: number;
remark?: string;
}) {
return requestClient.post<any>(`${prefix}stock-out`, data);
}
/** 出入库流水 */
export async function getDeliveryWarehouseStockLogList(data: Record<string, any>) {
return requestClient.get<any>(`${prefix}stock-log-list`, { params: data });
}

View File

@@ -0,0 +1,350 @@
<script lang="ts" setup>
/**
* 某药品的配送仓绑定列表弹窗(卡片展示)
* 展示报价/推广费/平台费/库存;新增/编辑打开仓药绑定表单(药品预填)
* 打开时尽量补齐总仓售价,供费用封顶与平台费默认 5%
*/
import { computed, nextTick, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Button, Empty, message, Pagination, Spin, Tag } from 'ant-design-vue';
import { getWarehouseDrugManagementList } from '#/views/business/warehouse-drug-management/admin/api';
import { deleteDeliveryWarehouseDrug, getDeliveryWarehouseDrugList } from '../api';
import { resolveCentralPrice } from '../utils/resolve-central-price';
import FormModal from './modal.vue';
const drugId = ref(0);
const drugName = ref('');
const drugMeta = ref<Record<string, any>>({});
/** 总仓售价,用于绑仓表单默认平台费与封顶 */
const centralPrice = ref<number | null>(null);
const productGridApi = ref();
const loading = ref(false);
const items = ref<Record<string, any>[]>([]);
const page = ref(1);
const pageSize = ref(10);
const total = ref(0);
/** 打开后是否立刻弹出新增表单 */
const pendingAutoCreate = ref(false);
const [FormModalComp, formModalApi] = useVbenModal({
connectedComponent: FormModal,
});
const priceText = computed(() => {
if (centralPrice.value != null && centralPrice.value > 0) {
return `¥${Number(centralPrice.value).toFixed(2)}`;
}
return '无总仓售价';
});
/**
* 加载绑定卡片数据
*/
async function loadList() {
if (!drugId.value) {
items.value = [];
total.value = 0;
return;
}
loading.value = true;
try {
const res = await getDeliveryWarehouseDrugList({
page: page.value,
pageSize: pageSize.value,
drug_id: drugId.value,
});
items.value = res?.items || [];
total.value = Number(res?.total || 0);
} finally {
loading.value = false;
}
}
/**
* 列表未带售价时,按 drug_id 查总仓补齐
*/
async function ensureCentralPrice() {
if (centralPrice.value != null && centralPrice.value > 0) return;
if (!drugId.value) return;
try {
const res = await getWarehouseDrugManagementList({
page: 1,
pageSize: 1,
drug_id: drugId.value,
});
const row = res?.items?.[0];
const price = Number(row?.price ?? 0);
if (price > 0) {
centralPrice.value = price;
}
} catch {
// 补齐失败不阻断绑仓,表单内仍可手填三费
}
}
/**
* 打开新增:药品锁定预填 + 带上 number 类型总仓售价
*/
async function openCreate() {
await ensureCentralPrice();
const price =
centralPrice.value != null && centralPrice.value > 0
? Number(centralPrice.value)
: null;
formModalApi.setData({
update: false,
// 新增成功后刷新业务列表与本卡片列表
gridApi: {
query: () => {
loadList();
productGridApi.value?.query?.();
},
reload: () => {
loadList();
productGridApi.value?.query?.();
},
},
lockDrug: true,
central_price: price,
presetDrug: {
id: drugId.value,
drug_name: drugName.value,
specification: drugMeta.value.specification || '',
image: drugMeta.value.image || '',
central_price: price,
},
values: {
drug_id: drugId.value,
...(price != null ? { sale_price: price } : {}),
},
});
formModalApi.open();
}
async function openEdit(row: any) {
await ensureCentralPrice();
const price =
centralPrice.value != null && centralPrice.value > 0
? Number(centralPrice.value)
: resolveCentralPrice(row);
formModalApi.setData({
update: true,
gridApi: {
query: () => {
loadList();
productGridApi.value?.query?.();
},
reload: () => {
loadList();
productGridApi.value?.query?.();
},
},
central_price: price,
values: {
...row,
...(price != null ? { sale_price: price } : {}),
},
});
formModalApi.open();
}
function handleDelete(id: number) {
deleteDeliveryWarehouseDrug({ ids: [id] }).then(() => {
message.success('删除成功');
loadList();
productGridApi.value?.query?.();
});
}
function onPageChange(p: number) {
page.value = p;
loadList();
}
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
draggable: true,
showConfirmButton: false,
onCancel() {
modalApi.close();
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
productGridApi.value?.query?.();
pendingAutoCreate.value = false;
return;
}
const data = modalApi.getData<Record<string, any>>() || {};
drugId.value = Number(data.drug_id || data.values?.id || 0);
drugName.value =
data.drug_name || data.values?.drug_name || `药品#${drugId.value}`;
drugMeta.value = {
specification: data.specification || data.values?.specification || '',
image: data.image || data.values?.image || '',
};
// 与表单同源解析:兼容嵌套 central_warehouse_drug
centralPrice.value =
resolveCentralPrice(data) ??
resolveCentralPrice(data.values) ??
resolveCentralPrice(data.presetDrug) ??
null;
productGridApi.value = data.gridApi;
pendingAutoCreate.value = !!data.autoCreate;
page.value = 1;
await ensureCentralPrice();
await loadList();
if (pendingAutoCreate.value) {
pendingAutoCreate.value = false;
await nextTick();
await openCreate();
}
},
});
</script>
<template>
<Modal :title="`配送仓绑定 - ${drugName}`" class="w-[720px]">
<FormModalComp />
<div class="bind-list-header">
<div class="bind-list-header__meta">
<span>药品售价</span>
<span
:class="
centralPrice && centralPrice > 0
? 'bind-list-header__price'
: 'bind-list-header__price--empty'
"
>
{{ priceText }}
</span>
</div>
<Button type="primary" @click="openCreate">新增绑定</Button>
</div>
<Spin :spinning="loading">
<div v-if="items.length" class="bind-card-list">
<div v-for="row in items" :key="row.id" class="bind-card">
<div class="bind-card__head">
<div class="bind-card__title">
{{ row.warehouse?.name || `仓库#${row.warehouse_id}` }}
</div>
<Tag v-if="row.status === 2" color="green">上架</Tag>
<Tag v-else color="default">下架</Tag>
</div>
<div class="bind-card__body">
<div class="bind-card__item">
<span class="bind-card__label">报价</span>
<span>¥{{ Number(row.quote || 0).toFixed(4) }}</span>
</div>
<div class="bind-card__item">
<span class="bind-card__label">推广费</span>
<span>¥{{ Number(row.promo_fee || 0).toFixed(4) }}</span>
</div>
<div class="bind-card__item">
<span class="bind-card__label">平台费</span>
<span>¥{{ Number(row.platform_fee || 0).toFixed(4) }}</span>
</div>
<div class="bind-card__item">
<span class="bind-card__label">库存</span>
<span>{{ row.stock ?? 0 }}</span>
</div>
</div>
<div class="bind-card__actions">
<Button type="link" size="small" @click="openEdit(row)">编辑</Button>
<Button
type="link"
size="small"
danger
@click="handleDelete(row.id)"
>
删除
</Button>
</div>
</div>
</div>
<Empty v-else description="暂无绑定" />
</Spin>
<div v-if="total > pageSize" class="bind-list-pager">
<Pagination
:current="page"
:page-size="pageSize"
:total="total"
size="small"
@change="onPageChange"
/>
</div>
</Modal>
</template>
<style scoped>
.bind-list-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 16px;
}
.bind-list-header__meta {
font-size: 13px;
color: hsl(var(--muted-foreground));
}
.bind-list-header__price {
font-weight: 600;
color: hsl(var(--foreground));
}
.bind-list-header__price--empty {
color: hsl(var(--muted-foreground));
}
.bind-card-list {
display: flex;
flex-direction: column;
gap: 12px;
max-height: 420px;
overflow-y: auto;
}
.bind-card {
border: 1px solid hsl(var(--border));
border-radius: 8px;
background: hsl(var(--card));
padding: 12px 14px;
}
.bind-card__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 10px;
}
.bind-card__title {
font-weight: 600;
color: hsl(var(--foreground));
}
.bind-card__body {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px 16px;
font-size: 13px;
color: hsl(var(--foreground));
}
.bind-card__item {
display: flex;
justify-content: space-between;
gap: 8px;
}
.bind-card__label {
color: hsl(var(--muted-foreground));
}
.bind-card__actions {
display: flex;
justify-content: flex-end;
margin-top: 8px;
border-top: 1px solid hsl(var(--border));
padding-top: 4px;
}
.bind-list-pager {
display: flex;
justify-content: flex-end;
margin-top: 12px;
}
</style>

View File

@@ -0,0 +1,308 @@
<script lang="ts" setup>
/**
* 仓药绑定弹窗
* 新增时用 DrugSearchSelect(is-card) 选药;编辑/预填锁定时不可换药
* 有总仓售价时:展示、新增默认平台费=售价5%、三费超售价实时提醒并拦截提交
* 注意:必须 await setValues否则异步覆盖会把 sale_price 冲掉
*/
import { nextTick, onMounted, onUnmounted, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { DrugSearchSelect } from '#/components/drug-search-select';
import {
createDeliveryWarehouseDrug,
updateDeliveryWarehouseDrug,
} from '#/views/system/delivery-warehouse-drug/api';
import {
calcFeeSum,
isFeeOverSalePrice,
modalFormProps,
registerFeeOverTipHandler,
syncFeeSumDisplay,
} from '#/views/system/delivery-warehouse-drug/config/form';
import { resolveCentralPrice } from '#/views/system/delivery-warehouse-drug/utils/resolve-central-price';
/** 非中药品类:西药/保健/中成药/服务包/非药品/医疗器械 */
const NON_CHINESE_DRUG_TYPES = [2, 3, 4, 5, 6, 7];
/** 只读卡片占位图 */
const DRUG_PLACEHOLDER_IMAGE =
'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk_upload_20250510/cd470ebf97e31c4048ba502ba71b66a3.jpg';
const isUpdate = ref(false);
/** 从药品列表打开时锁定药品 */
const lockDrug = ref(false);
const gridApi = ref();
/** 三费超售价时的醒目红色提示(空串表示无提示) */
const feeOverTip = ref('');
const editDrugCard = ref<{
name: string;
id: number | null;
spec: string;
image: string;
} | null>(null);
const [Form, formApi] = useVbenForm(modalFormProps);
// 表单 dependencies 触发 sync 时同步到弹窗红色文案
onMounted(() => {
registerFeeOverTipHandler((tip) => {
feeOverTip.value = tip;
});
});
onUnmounted(() => {
registerFeeOverTipHandler(null);
});
/**
* 写入售价;新增时默认平台费 = 售价 5%
*/
async function applyCentralPrice(
price: number | string | null | undefined,
isCreate: boolean,
) {
const n = Number(price);
if (!(n > 0)) {
await formApi.setFieldValue('sale_price', undefined);
return;
}
const sale = Number(n.toFixed(4));
await formApi.setFieldValue('sale_price', sale);
if (isCreate) {
await formApi.setFieldValue('platform_fee', Number((sale * 0.05).toFixed(4)));
}
}
/**
* 从弹窗入参解析售价:优先 data.central_price再嵌套/values/preset
*/
function pickSalePrice(data: Record<string, any>) {
const fromData = resolveCentralPrice(data);
if (fromData != null) return fromData;
const fromValues = resolveCentralPrice(data.values);
if (fromValues != null) return fromValues;
return resolveCentralPrice(data.presetDrug);
}
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
draggable: true,
onCancel() {
modalApi.close();
},
onConfirm: async () => {
const values = await formApi.getValues();
if (!isUpdate.value && !values.drug_id) {
message.error('请选择药品');
return;
}
// 提交前实时拦截:三费合计不得超过药品售价
if (isFeeOverSalePrice(values)) {
message.warning(
`三费合计已超过药品售价 ¥${Number(values.sale_price).toFixed(4)}`,
);
return;
}
formApi.validate().then(async (e: any) => {
if (e.valid) {
const payload = { ...(await formApi.getValues()) };
// sale_price 只作封顶校验,不用费用合计覆盖
if (payload.sale_price === undefined || payload.sale_price === '') {
delete payload.sale_price;
}
delete payload._fee_sum;
if (Number(calcFeeSum(payload)) <= 0) {
message.warning('报价/推广费/平台费合计必须大于0');
return;
}
modalApi.setState({ loading: true, confirmLoading: true });
const submitApi = isUpdate.value
? updateDeliveryWarehouseDrug
: createDeliveryWarehouseDrug;
submitApi(payload)
.then(() => {
message.success('保存成功');
gridApi.value?.query?.() ?? gridApi.value?.reload?.();
modalApi.close();
})
.finally(() => {
modalApi.setState({ loading: false, confirmLoading: false });
});
}
});
await formApi.validateAndSubmitForm();
},
async onOpenChange(isOpen: boolean) {
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
if (!isOpen) {
feeOverTip.value = '';
return;
}
const data = modalApi.getData<Record<string, any>>() || {};
const { values, update, presetDrug } = data;
isUpdate.value = !!update;
lockDrug.value = !!data.lockDrug && !update;
editDrugCard.value = null;
feeOverTip.value = '';
const salePrice = pickSalePrice(data);
// 先重置,再 await 写入,避免异步 setValues 冲掉 sale_price
await formApi.resetForm();
const nextValues = {
...(values || {}),
...(salePrice != null ? { sale_price: salePrice } : {}),
};
if (Object.keys(nextValues).length) {
await formApi.setValues(nextValues);
}
if (update && values) {
editDrugCard.value = {
id: values.drug_id ? Number(values.drug_id) : null,
name:
values.drug?.drug_name ||
values.drug_name ||
(values.drug_id ? `药品#${values.drug_id}` : ''),
spec: values.drug?.specification || values.specification || '',
image: values.drug?.image || values.image || DRUG_PLACEHOLDER_IMAGE,
};
// 编辑:只写售价,不覆盖已有三费
await applyCentralPrice(salePrice, false);
} else if (lockDrug.value && presetDrug) {
const id = Number(presetDrug.id || values?.drug_id || 0);
await formApi.setFieldValue('drug_id', id);
editDrugCard.value = {
id,
name: presetDrug.drug_name || `药品#${id}`,
spec: presetDrug.specification || '',
image: presetDrug.image || DRUG_PLACEHOLDER_IMAGE,
};
await applyCentralPrice(salePrice, true);
} else if (!update) {
await applyCentralPrice(salePrice, true);
}
// 再确认一次:部分表单实现会二次回填空值
await nextTick();
const after = await formApi.getValues();
const afterSale = Number(after?.sale_price);
if (salePrice != null && (!(afterSale > 0) || afterSale !== salePrice)) {
await applyCentralPrice(salePrice, !update);
}
// 打开后同步一次上限与提示(售价写入后 dependencies 可能未立刻跑完)
const latest = await formApi.getValues();
feeOverTip.value = syncFeeSumDisplay(latest, formApi);
},
});
async function onSelectDrug(item: any) {
if (isUpdate.value || lockDrug.value) return;
if (!item) {
await formApi.setFieldValue('drug_id', undefined);
return;
}
const drugId = Number(item?.drug?.id || item?.drug_id || item?.id || 0);
if (!drugId) {
message.error('药品数据异常');
return;
}
await formApi.setFieldValue('drug_id', drugId);
// 搜药选中后若带总仓价则预填平台费 5%
const price = resolveCentralPrice(item) ?? resolveCentralPrice(item?.drug);
if (price != null) {
await applyCentralPrice(price, true);
}
}
</script>
<template>
<Modal
:title="`${isUpdate === true ? '编辑' : '新增'}仓药绑定`"
class="w-[40%]"
>
<div class="mb-4">
<div class="mb-1 text-sm text-gray-600">
药品
<span class="text-red-500">*</span>
</div>
<div
v-if="(isUpdate || lockDrug) && editDrugCard"
class="warehouse-drug-readonly-card"
>
<img
:src="editDrugCard.image || DRUG_PLACEHOLDER_IMAGE"
alt=""
class="warehouse-drug-readonly-card__img"
/>
<div class="warehouse-drug-readonly-card__info">
<div class="warehouse-drug-readonly-card__name">
{{ editDrugCard.name || `药品ID${editDrugCard.id}` }}
</div>
<div class="warehouse-drug-readonly-card__meta">
<span v-if="editDrugCard.id">ID{{ editDrugCard.id }}</span>
<span v-if="editDrugCard.spec">规格{{ editDrugCard.spec }}</span>
<span class="warehouse-drug-readonly-card__hint">
{{ isUpdate ? '编辑时不可更换药品' : '已自动选中当前药品' }}
</span>
</div>
</div>
</div>
<DrugSearchSelect
v-else
:types="NON_CHINESE_DRUG_TYPES"
:is-card="true"
placeholder="聚焦或搜索药品(不含中药)"
@select="onSelectDrug"
/>
</div>
<!-- 超售价醒目提示不能只靠 InputNumber max -->
<div v-if="feeOverTip" class="fee-over-tip">{{ feeOverTip }}</div>
<Form />
</Modal>
</template>
<style scoped>
.fee-over-tip {
margin-bottom: 12px;
font-size: 13px;
line-height: 1.5;
color: hsl(var(--destructive));
}
/* 只用框架主题变量,避免硬编码 / :global(.dark) 污染全局配色 */
.warehouse-drug-readonly-card {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: hsl(var(--muted) / 0.3);
border: 1px solid hsl(var(--border));
border-radius: 8px;
}
.warehouse-drug-readonly-card__img {
width: 48px;
height: 48px;
object-fit: cover;
border-radius: 6px;
border: 1px solid hsl(var(--border));
flex-shrink: 0;
}
.warehouse-drug-readonly-card__info {
flex: 1;
min-width: 0;
}
.warehouse-drug-readonly-card__name {
font-size: 14px;
font-weight: 500;
color: hsl(var(--foreground));
margin-bottom: 4px;
}
.warehouse-drug-readonly-card__meta {
display: flex;
flex-wrap: wrap;
gap: 12px;
font-size: 12px;
color: hsl(var(--muted-foreground));
}
.warehouse-drug-readonly-card__hint {
color: hsl(var(--muted-foreground));
}
</style>

View File

@@ -0,0 +1,222 @@
import type { VbenFormProps } from '#/adapter/form';
import { getDeliveryWarehouseOption } from '#/views/system/delivery-warehouse/api';
/** 计算三费合计,用于表单联动展示 */
export function calcFeeSum(values: Record<string, any>) {
const quote = Number(values.quote) || 0;
const promoFee = Number(values.promo_fee) || 0;
const platformFee = Number(values.platform_fee) || 0;
return (quote + promoFee + platformFee).toFixed(4);
}
/** 三费是否超过药品售价 */
export function isFeeOverSalePrice(values: Record<string, any>) {
const sale = Number(values.sale_price);
if (!(sale > 0)) return false;
return Number(calcFeeSum(values)) > sale;
}
/** 超售价文案回调(由 modal 注册,用于醒目红色提示) */
type FeeOverTipHandler = (tip: string) => void;
let feeOverTipHandler: FeeOverTipHandler | null = null;
/** 注册/注销弹窗内的超售价文本提示处理器 */
export function registerFeeOverTipHandler(handler: FeeOverTipHandler | null) {
feeOverTipHandler = handler;
}
/**
* 费用合计联动:实时重算、三费 max、超售价 help + 文本提示
* 为什么放在 trigger输入任三费即触发不必等提交
* @returns 超售价提示文案(无则空串)
*/
export function syncFeeSumDisplay(
values: Record<string, any>,
formApi: any,
): string {
const sum = calcFeeSum(values);
formApi.setFieldValue('_fee_sum', sum);
const sale = Number(values.sale_price) || 0;
const over = sale > 0 && Number(sum) > sale;
const tip = over
? `三费合计已超过药品售价 ¥${sale.toFixed(4)},请下调报价/推广费/平台费`
: '';
const feeHelp = over
? tip
: sale > 0
? `报价+推广费+平台费之和不得超过药品售价 ¥${sale.toFixed(4)}`
: '报价+推广费+平台费之和保存时需大于0';
// 单项输入上限:有售价时不得超过售价;同时刷新合计 help
const feeInputProps = {
min: 0,
precision: 4,
class: 'w-full',
...(sale > 0 ? { max: sale } : {}),
};
formApi.updateSchema([
{
fieldName: 'quote',
componentProps: {
...feeInputProps,
placeholder: '请输入报价',
},
},
{
fieldName: 'promo_fee',
componentProps: {
...feeInputProps,
placeholder: '请输入推广费',
},
},
{
fieldName: 'platform_fee',
componentProps: {
...feeInputProps,
placeholder: '请输入平台费',
},
},
{
fieldName: '_fee_sum',
help: feeHelp,
componentProps: {
disabled: true,
placeholder: '报价+推广费+平台费',
style: over ? { color: 'hsl(var(--destructive))' } : undefined,
},
},
]);
feeOverTipHandler?.(tip);
return tip;
}
export const modalFormProps: VbenFormProps = {
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: 'ApiSelect',
componentProps: {
allowClear: true,
showSearch: true,
filterOption: (input: string, option: any) =>
option?.label.toLowerCase().indexOf(input.toLowerCase()) >= 0,
afterFetch: (data: { id: number; name: string }[]) =>
data.map((item: any) => ({
label: item.name,
value: item.id,
})),
api: getDeliveryWarehouseOption,
placeholder: '请选择配送仓库',
},
fieldName: 'warehouse_id',
label: '配送仓库',
rules: 'required',
},
{
// 由弹窗顶部 DrugSearchSelect 写入,表单内隐藏避免手填
component: 'InputNumber',
fieldName: 'drug_id',
label: '药品ID',
rules: 'required',
dependencies: {
show: false,
triggerFields: ['drug_id'],
},
},
{
// 总仓售价只读展示,同时作为提交时的 sale_price 封顶基准
component: 'InputNumber',
componentProps: {
disabled: true,
precision: 4,
class: 'w-full',
placeholder: '无总仓售价',
},
fieldName: 'sale_price',
label: '药品售价',
help: '来自总仓库建议售价;三费合计不得超过该值',
},
{
component: 'InputNumber',
componentProps: {
placeholder: '请输入报价',
min: 0,
precision: 4,
class: 'w-full',
},
fieldName: 'quote',
label: '报价',
rules: 'required',
},
{
component: 'InputNumber',
componentProps: {
placeholder: '请输入推广费',
min: 0,
precision: 4,
class: 'w-full',
},
fieldName: 'promo_fee',
label: '推广费',
rules: 'required',
},
{
component: 'InputNumber',
componentProps: {
placeholder: '请输入平台费',
min: 0,
precision: 4,
class: 'w-full',
},
fieldName: 'platform_fee',
label: '平台费',
rules: 'required',
},
{
component: 'VbenInput',
componentProps: {
disabled: true,
placeholder: '报价+推广费+平台费',
},
fieldName: '_fee_sum',
label: '费用合计',
help: '报价+推广费+平台费之和保存时需大于0',
dependencies: {
trigger(values, formApi) {
syncFeeSumDisplay(values, formApi);
},
triggerFields: ['quote', 'promo_fee', 'platform_fee', 'sale_price'],
},
},
// 绑定创建时库存固定为 0后续在「库存管理」入库/出库,禁止表单直改数量
{
component: 'RadioGroup',
componentProps: {
options: [
{ label: '下架', value: 1 },
{ label: '上架', value: 2 },
],
},
fieldName: 'status',
label: '状态',
defaultValue: 2,
},
],
showDefaultActions: false,
};

View File

@@ -0,0 +1,41 @@
import type { VbenFormProps } from '#/adapter/form';
import { getDeliveryWarehouseOption } from '#/views/system/delivery-warehouse/api';
export const formOptions: VbenFormProps = {
collapsed: false,
schema: [
{
component: 'ApiSelect',
componentProps: {
allowClear: true,
showSearch: true,
filterOption: (input: string, option: any) =>
option?.label.toLowerCase().indexOf(input.toLowerCase()) >= 0,
afterFetch: (data: { id: number; name: string }[]) =>
data.map((item: any) => ({
label: item.name,
value: item.id,
})),
api: getDeliveryWarehouseOption,
placeholder: '请选择配送仓库',
},
fieldName: 'warehouse_id',
label: '配送仓库',
},
{
component: 'VbenInput',
componentProps: {
placeholder: '输入药品ID',
},
fieldName: 'drug_id',
label: '药品ID',
},
],
showCollapseButton: true,
submitButtonOptions: {
content: '查询',
},
submitOnChange: true,
submitOnEnter: false,
};

View File

@@ -0,0 +1,89 @@
import type { VxeGridProps } from '#/adapter/vxe-table';
import { getDeliveryWarehouseDrugList } from '#/views/system/delivery-warehouse-drug/api';
interface RowType {
id: string;
warehouse_id: number;
drug_id: number;
quote: string;
promo_fee: string;
platform_fee: string;
stock: number;
frozen_number: number;
status: number;
created_at: string;
drug?: { drug_name: string };
warehouse?: { name: string };
}
export const gridOptions: VxeGridProps<RowType> = {
checkboxConfig: {
highlight: true,
labelField: '',
},
columnConfig: {
useKey: true,
},
rowConfig: {
useKey: true,
},
columns: [
{ type: 'checkbox', width: 60 },
{ field: 'id', align: 'left', title: 'ID', width: 100 },
{
field: 'warehouse_id',
title: '配送仓库',
slots: { default: 'warehouse_id' },
},
{ field: 'drug_id', title: '药品ID', width: 100 },
{
field: 'drug_name',
title: '药品名称',
slots: { default: 'drug_name' },
},
{ field: 'quote', title: '报价' },
{ field: 'promo_fee', title: '推广费' },
{ field: 'platform_fee', title: '平台费' },
{ field: 'stock', title: '库存' },
{ field: 'frozen_number', title: '冻结数量' },
{
field: 'status',
title: '状态',
slots: { default: 'status' },
width: 100,
},
{ field: 'created_at', title: '创建时间' },
{ type: 'html', title: '操作', slots: { default: 'action' } },
],
keepSource: true,
pagerConfig: {},
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getDeliveryWarehouseDrugList({
page: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
height: 'auto',
border: false,
toolbarConfig: {
// @ts-ignore
search: true,
refresh: true,
print: false,
export: false,
zoom: true,
slots: {
buttons: 'toolbar-buttons',
},
custom: {
icon: 'vxe-icon-menu',
},
},
showOverflow: false,
};

View File

@@ -0,0 +1,135 @@
<script lang="ts" setup>
import type { VxeGridListeners } from '#/adapter/vxe-table';
import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { Button, message, Tag } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { TableAction } from '#/components/table-action';
import { deleteDeliveryWarehouseDrug } from './api';
import FormModalDemo from './components/modal.vue';
import { formOptions } from './config/search';
import { gridOptions } from './config/table';
const hasTopTableDropDownActions = ref(false);
const gridEvents: VxeGridListeners<any> = {
checkboxChange() {
const records = gridApi.grid.getCheckboxRecords();
hasTopTableDropDownActions.value = records.length > 0;
},
checkboxAll() {
const records = gridApi.grid.getCheckboxRecords();
hasTopTableDropDownActions.value = records.length > 0;
},
};
const [Grid, gridApi] = useVbenVxeGrid({
formOptions,
gridOptions,
gridEvents,
});
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: FormModalDemo,
});
const showModal = (data = {}, isUpdate = false) => {
formModalApi.setData({
values: data,
update: isUpdate,
gridApi,
});
formModalApi.open();
};
const deleteApi = (row: any) => {
let ids = [];
if (row) {
ids.push(row);
} else {
ids = gridApi.grid.getCheckboxRecords().map((item) => item.id);
}
deleteDeliveryWarehouseDrug({ ids }).then(() => {
message.success('删除成功!');
gridApi.query();
});
};
</script>
<template>
<Page auto-content-height title="仓药绑定管理">
<FormModal />
<Grid>
<template #toolbar-buttons>
<TableAction
:actions="[
{
label: '新增',
type: 'primary',
icon: 'ant-design:plus-outlined',
onClick: showModal.bind(null),
},
]"
:drop-down-actions="[
{
label: '删除',
icon: 'ant-design:delete-outlined',
ifShow: hasTopTableDropDownActions,
popConfirm: {
title: '确定删除吗',
confirm: deleteApi.bind(null, false),
},
},
]"
>
<template #more>
<Button style="margin-left: 16px">
批量操作
<Icon icon="ant-design:down-outlined" />
</Button>
</template>
</TableAction>
</template>
<template #warehouse_id="{ row }">
{{ row.warehouse?.name || row.warehouse_id }}
</template>
<template #drug_name="{ row }">
{{ row.drug?.drug_name || '-' }}
</template>
<template #status="{ row }">
<Tag v-if="row.status === 2" color="green">上架</Tag>
<Tag v-else color="default">下架</Tag>
</template>
<template #toolbar-tools></template>
<template #action="{ row }">
<TableAction
:actions="[
{
label: '编辑',
type: 'link',
icon: 'uil:edit',
size: 'small',
onClick: showModal.bind(null, row, true),
},
{
label: '删除',
type: 'link',
icon: 'ant-design:delete-outlined',
size: 'small',
popConfirm: {
title: '确定删除吗?',
confirm: deleteApi.bind(null, row.id),
},
},
]"
:drop-down-actions="[]"
/>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,23 @@
/**
* 从列表行 / 弹窗入参中解析总仓售价
* 兼容 snake / camel 及嵌套 central_warehouse_drug
*/
export function resolveCentralPrice(
source: Record<string, any> | null | undefined,
): number | null {
if (!source) return null;
const nested =
source.central_warehouse_drug ?? source.centralWarehouseDrug ?? null;
const candidates = [
source.central_price,
source.centralPrice,
nested?.price,
source.sale_price,
source.price,
];
for (const c of candidates) {
const n = Number(c);
if (n > 0) return Number(n.toFixed(4));
}
return null;
}

View File

@@ -0,0 +1,31 @@
import { requestClient } from '#/api/request';
const prefix = 'delivery-warehouse-order/';
/**
* 配送仓库订单列表
*/
export async function getDeliveryWarehouseOrderList(data: any) {
return requestClient.get<any>(`${prefix}list`, { params: data });
}
/**
* 配送仓库订单详情(本仓药品 + 收货/物流基础字段)
*/
export async function getDeliveryWarehouseOrderDetail(id: number | string) {
return requestClient.get<any>(`${prefix}detail`, { params: { id } });
}
/**
* 按订单查物流轨迹(复用平台 express-detail 接口)
*/
export async function expressDetailByOrderId(data: Record<string, any>) {
return requestClient.post<any>(`express-detail/detail-by-order`, data);
}
/**
* 配送仓库订单发货
*/
export async function sendDeliveryWarehouseOrder(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}ware-send`, data);
}

View File

@@ -0,0 +1,321 @@
<script lang="ts" setup>
import { computed, nextTick, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Button, Descriptions, Image, Tag, Timeline } from 'ant-design-vue';
import {
expressDetailByOrderId,
getDeliveryWarehouseOrderDetail,
} from '#/views/system/delivery-warehouse-order/api';
defineOptions({
name: 'DeliveryWarehouseOrderDetailModal',
});
const data = ref<Record<string, any>>({});
const expressDetail = ref<Record<string, any>>({});
const scrollToLogistics = ref(false);
const logisticsAnchorRef = ref<HTMLElement | null>(null);
/** 待发货时由列表传入,点击后关详情并打开发货弹窗 */
const onShipFn = ref<null | (() => void)>(null);
const canShip = computed(
() => Number(data.value.status) === 1 && typeof onShipFn.value === 'function',
);
const statusMap: Record<number, { text: string; color: string }> = {
0: { text: '待支付', color: 'default' },
1: { text: '待发货', color: 'orange' },
2: { text: '待收货', color: 'blue' },
3: { text: '待评价', color: 'cyan' },
4: { text: '已退款', color: 'red' },
5: { text: '退款中', color: 'volcano' },
6: { text: '已收货', color: 'green' },
7: { text: '确认收货', color: 'green' },
8: { text: '拒绝退款', color: 'gold' },
9: { text: '已取消', color: 'default' },
};
const deliveryMethodMap: Record<number, string> = {
0: '快递邮寄',
1: '到店自取',
};
/**
* 拉取订单详情;已发货时继续拉物流轨迹
*/
async function loadDetail(id: number | string) {
data.value = (await getDeliveryWarehouseOrderDetail(id)) || {};
expressDetail.value = {};
const sent =
Number(data.value.is_send) === 1 ||
Number(data.value.status) >= 2 ||
!!data.value.express_no_id;
if (sent && data.value.id) {
try {
expressDetail.value = await expressDetailByOrderId({
order_id: data.value.id,
});
} catch {
expressDetail.value = {};
}
}
if (scrollToLogistics.value) {
await nextTick();
logisticsAnchorRef.value?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
function getColor(status: any) {
switch (status) {
case '揽收': {
return 'orange';
}
case '派件': {
return 'blue';
}
case '签收': {
return 'green';
}
default: {
return '';
}
}
}
const expressNoRow = () =>
data.value.express_no || data.value.expressNo || {};
function handleShip() {
const fn = onShipFn.value;
if (typeof fn === 'function') {
fn();
}
}
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
draggable: true,
showConfirmButton: false,
onCancel() {
modalApi.close();
},
onOpenChange(isOpen: boolean) {
if (!isOpen) {
data.value = {};
expressDetail.value = {};
scrollToLogistics.value = false;
onShipFn.value = null;
return;
}
const payload = modalApi.getData<Record<string, any>>() || {};
scrollToLogistics.value = !!payload.scrollToLogistics;
onShipFn.value =
typeof payload.onShip === 'function' ? payload.onShip : null;
const id = payload.id || payload.values?.id;
if (id) {
loadDetail(id);
}
},
});
</script>
<template>
<Modal title="订单详情" class="w-[720px]">
<div class="wh-order-detail space-y-5">
<div v-if="canShip" class="flex justify-end">
<Button type="primary" @click="handleShip">发货</Button>
</div>
<section>
<h3 class="section-title">收货人信息</h3>
<Descriptions
:column="{ xxl: 2, xl: 2, lg: 2, md: 1, sm: 1, xs: 1 }"
bordered
size="small"
>
<Descriptions.Item label="收货人">
{{ data.express_name || '-' }}
</Descriptions.Item>
<Descriptions.Item label="联系电话">
{{ data.express_mobile || '-' }}
</Descriptions.Item>
<Descriptions.Item label="收货地区" :span="2">
{{ data.express_region || '-' }}
</Descriptions.Item>
<Descriptions.Item label="详细地址" :span="2">
{{ data.express_address || '-' }}
</Descriptions.Item>
</Descriptions>
</section>
<section>
<h3 class="section-title">订单信息</h3>
<Descriptions
:column="{ xxl: 2, xl: 2, lg: 2, md: 1, sm: 1, xs: 1 }"
bordered
size="small"
>
<Descriptions.Item label="订单号">
{{ data.order_no || '-' }}
</Descriptions.Item>
<Descriptions.Item label="订单状态">
<Tag :color="statusMap[data.status]?.color || 'default'">
{{ statusMap[data.status]?.text || data.status || '-' }}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="邮寄方式">
{{
deliveryMethodMap[data.delivery_method] ??
data.delivery_method ??
'-'
}}
</Descriptions.Item>
<Descriptions.Item label="下单时间">
{{ data.created_at || '-' }}
</Descriptions.Item>
<Descriptions.Item label="发货时间">
{{ data.send_time || '-' }}
</Descriptions.Item>
</Descriptions>
</section>
<section>
<h3 class="section-title">药品信息本仓</h3>
<div
v-if="Array.isArray(data.products) && data.products.length"
class="product-list"
>
<div
v-for="item in data.products"
:key="item.id"
class="product-row"
>
<div class="product-cover">
<Image
:src="item.drug?.image || item.drug_image || '/img/empty.png'"
alt=""
height="64"
width="64"
/>
</div>
<div class="product-meta">
<div class="product-name">{{ item.drug_name || '-' }}</div>
<div class="product-sub">
规格{{ item.drug?.specification || '-' }}
</div>
<div class="product-sub">
数量{{ item.number ?? '-' }}
<span v-if="item.dosage != null && item.dosage !== ''">
剂量{{ item.dosage }}
</span>
</div>
</div>
</div>
</div>
<div v-else class="empty-tip">暂无本仓药品</div>
</section>
<section ref="logisticsAnchorRef">
<h3 class="section-title">物流信息</h3>
<template
v-if="
Number(data.is_send) === 1 ||
Number(data.status) >= 2 ||
expressNoRow().express_no ||
expressDetail.express_no
"
>
<Descriptions
:column="{ xxl: 2, xl: 2, lg: 2, md: 1, sm: 1, xs: 1 }"
bordered
size="small"
>
<Descriptions.Item label="物流公司">
{{
expressDetail.express_company_name ||
expressNoRow().express_company_name ||
'-'
}}
</Descriptions.Item>
<Descriptions.Item label="运单号">
{{
expressDetail.express_no || expressNoRow().express_no || '-'
}}
</Descriptions.Item>
<Descriptions.Item label="最新状态">
{{ expressDetail.state_txt || '-' }}
</Descriptions.Item>
</Descriptions>
<div v-if="Array.isArray(expressDetail.detail) && expressDetail.detail.length" class="mt-4">
<h4 class="mb-2 text-sm font-medium">物流追踪</h4>
<Timeline>
<Timeline.Item
v-for="(detail, index) in expressDetail.detail"
:key="index"
>
<Tag :color="getColor(detail.status)">
{{ detail.status }}
</Tag>
<p>{{ detail.detail_at }}</p>
<p>{{ detail.detail }}</p>
</Timeline.Item>
</Timeline>
</div>
<div v-else class="empty-tip mt-3">暂无物流轨迹</div>
</template>
<div v-else class="empty-tip">暂无物流</div>
</section>
</div>
</Modal>
</template>
<style scoped>
.section-title {
margin-bottom: 10px;
font-size: 15px;
font-weight: 600;
color: hsl(var(--foreground));
}
.product-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.product-row {
display: flex;
align-items: center;
gap: 12px;
padding: 10px;
border: 1px solid hsl(var(--border));
border-radius: 8px;
background: hsl(var(--card));
}
.product-cover {
width: 64px;
height: 64px;
overflow: hidden;
border-radius: 6px;
background: hsl(var(--muted));
flex-shrink: 0;
}
.product-meta {
min-width: 0;
flex: 1;
}
.product-name {
font-size: 14px;
font-weight: 500;
color: hsl(var(--foreground));
}
.product-sub {
margin-top: 2px;
font-size: 12px;
color: hsl(var(--muted-foreground));
}
.empty-tip {
font-size: 13px;
color: hsl(var(--muted-foreground));
}
</style>

View File

@@ -0,0 +1,57 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { sendDeliveryWarehouseOrder } from '#/views/system/delivery-warehouse-order/api';
import { modalFormProps } from '#/views/system/delivery-warehouse-order/config/form';
const gridApi = ref();
const orderNo = ref('');
const [Form, formApi] = useVbenForm(modalFormProps);
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
draggable: true,
onCancel() {
modalApi.close();
},
onConfirm: async () => {
formApi.validate().then(async (e: any) => {
if (e.valid) {
const values = await formApi.getValues();
modalApi.setState({ loading: true, confirmLoading: true });
sendDeliveryWarehouseOrder(values)
.then(() => {
message.success('发货成功');
gridApi.value?.reload();
modalApi.close();
})
.finally(() => {
modalApi.setState({ loading: false, confirmLoading: false });
});
}
});
await formApi.validateAndSubmitForm();
},
onOpenChange(isOpen: boolean) {
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
if (isOpen) {
const { values } = modalApi.getData<Record<string, any>>();
if (values) {
orderNo.value = values.order_no;
formApi.setValues(values);
}
}
},
});
</script>
<template>
<Modal :title="`订单:【 ${orderNo} 】发货`" class="w-[30%]">
<Form />
</Modal>
</template>

View File

@@ -0,0 +1,61 @@
import type { VbenFormProps } from '#/adapter/form';
export const modalFormProps: VbenFormProps = {
wrapperClass: 'grid-cols-12',
commonConfig: {
formItemClass: 'col-span-12',
componentProps: {
class: 'w-full',
},
},
layout: 'horizontal',
schema: [
{
component: 'VbenInput',
fieldName: 'order_id',
label: '订单ID',
dependencies: {
show: false,
triggerFields: ['order_id'],
},
},
{
component: 'VbenInput',
componentProps: {
placeholder: '请输入快递单号',
},
fieldName: 'express_no',
label: '快递单号',
rules: 'required',
},
{
component: 'ExpressCompanySelect',
componentProps: {
placeholder: '请选择快递公司',
},
fieldName: 'express_company_code',
formItemClass: 'col-span-6',
label: '快递公司',
rules: 'required',
// 单号变化时传入 trackingNo组件按前缀自动匹配快递公司
dependencies: {
triggerFields: ['express_no'],
componentProps(values) {
return {
placeholder: '请选择快递公司',
trackingNo: values.express_no,
};
},
},
},
{
component: 'Textarea',
componentProps: {
placeholder: '请输入发货备注',
},
fieldName: 'introduce',
label: '发货备注',
},
],
showDefaultActions: false,
};

View File

@@ -0,0 +1,22 @@
import type { VbenFormProps } from '#/adapter/form';
export const formOptions: VbenFormProps = {
collapsed: false,
schema: [
{
component: 'VbenInput',
componentProps: {
placeholder: '输入订单号',
},
defaultValue: '',
fieldName: 'order_no',
label: '订单号',
},
],
showCollapseButton: false,
submitButtonOptions: {
content: '查询',
},
submitOnChange: true,
submitOnEnter: false,
};

View File

@@ -0,0 +1,74 @@
import type { VxeGridProps } from '#/adapter/vxe-table';
import { getDeliveryWarehouseOrderList } from '#/views/system/delivery-warehouse-order/api';
interface RowType {
id: number;
order_no: string;
status: number;
express_name: string;
express_mobile: string;
express_region: string;
express_address: string;
created_at: string;
}
export const gridOptions: VxeGridProps<RowType> = {
columnConfig: {
useKey: true,
},
rowConfig: {
useKey: true,
},
columns: [
{ field: 'order_no', align: 'left', title: '订单号', minWidth: 160 },
{
field: 'status',
title: '状态',
slots: { default: 'status' },
width: 120,
},
{ field: 'express_name', title: '收货人' },
{ field: 'express_mobile', title: '联系电话' },
{ field: 'express_region', title: '收货地区' },
{ field: 'express_address', title: '收货地址', minWidth: 200 },
{
field: 'logistics_summary',
title: '物流',
minWidth: 160,
slots: { default: 'logistics' },
},
{ field: 'created_at', title: '下单时间', width: 180 },
{ type: 'html', title: '操作', slots: { default: 'action' }, width: 160 },
],
keepSource: true,
pagerConfig: {},
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getDeliveryWarehouseOrderList({
page: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
height: 'auto',
border: false,
toolbarConfig: {
// @ts-ignore
search: true,
refresh: true,
print: false,
export: false,
zoom: true,
slots: {
buttons: 'toolbar-buttons',
},
custom: {
icon: 'vxe-icon-menu',
},
},
showOverflow: false,
};

View File

@@ -0,0 +1,314 @@
<script lang="ts" setup>
import { computed, onMounted, reactive, ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import {
Button,
Card,
Empty,
Image,
Input,
Pagination,
Space,
Spin,
Tabs,
Tag,
} from 'ant-design-vue';
import { getDeliveryWarehouseOrderList } from '#/views/system/delivery-warehouse-order/api';
import DetailModalDemo from './components/detail-modal.vue';
import FormModalDemo from './components/modal.vue';
defineOptions({ name: 'DeliveryWarehouseOrderIndex' });
/** Tab待发货1 / 待收货2 / 确认收货7仅7 */
type StatusTab = '1' | '2' | '7';
const statusTab = ref<StatusTab>('1');
const orderNo = ref('');
const loading = ref(false);
const list = ref<Record<string, any>[]>([]);
const pager = reactive({
currentPage: 1,
pageSize: 12,
total: 0,
});
const statusMeta: Record<number, { text: string; color: string }> = {
0: { text: '待支付', color: 'default' },
1: { text: '待发货', color: 'orange' },
2: { text: '待收货', color: 'blue' },
3: { text: '待评价', color: 'cyan' },
4: { text: '已退款', color: 'red' },
5: { text: '退款中', color: 'volcano' },
6: { text: '已收货', color: 'green' },
7: { text: '确认收货', color: 'green' },
8: { text: '拒绝退款', color: 'gold' },
9: { text: '已取消', color: 'default' },
};
const tabLabel = computed(() => {
if (statusTab.value === '1') return '待发货';
if (statusTab.value === '2') return '待收货';
return '确认收货';
});
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: FormModalDemo,
});
const [DetailModal, detailModalApi] = useVbenModal({
connectedComponent: DetailModalDemo,
});
/**
* 加载当前 Tab 订单卡片列表
*/
async function loadList() {
loading.value = true;
try {
const res = await getDeliveryWarehouseOrderList({
page: pager.currentPage,
pageSize: pager.pageSize,
status: Number(statusTab.value),
order_no: orderNo.value?.trim() || undefined,
});
list.value = res?.items ?? res?.list ?? [];
pager.total = Number(res?.total ?? res?.pager?.total ?? 0);
} finally {
loading.value = false;
}
}
function handleTabChange(key: string | number) {
statusTab.value = String(key) as StatusTab;
pager.currentPage = 1;
void loadList();
}
function handleSearch() {
pager.currentPage = 1;
void loadList();
}
/** 列表刷新回调(发货成功后用) */
const gridApiLike = {
reload: () => loadList(),
query: () => loadList(),
};
/** 打开发货弹窗 */
function wareSend(row: Record<string, any>) {
formModalApi.setData({
values: {
order_id: row?.id,
order_no: row?.order_no,
},
gridApi: gridApiLike,
});
formModalApi.open();
}
/**
* 打开详情scrollToLogistics=true 时详情加载后滚到物流区块
* 待发货时传入 onShip详情内可直接打开发货弹窗
*/
function openDetail(row: Record<string, any>, scrollToLogistics = false) {
detailModalApi.setData({
id: row?.id,
scrollToLogistics,
onShip:
Number(row?.status) === 1
? () => {
detailModalApi.close();
wareSend(row);
}
: undefined,
});
detailModalApi.open();
}
/** 本仓商品行(图/名/规格/数量) */
function productRows(row: Record<string, any>) {
return Array.isArray(row.products) ? row.products : [];
}
/** 数量展示:有剂量时显示 number×dosage */
function qtyText(p: Record<string, any>) {
const n = p.number ?? '-';
if (p.dosage != null && p.dosage !== '' && Number(p.dosage) !== 1) {
return `${n}×${p.dosage}`;
}
return String(n);
}
function addressText(row: Record<string, any>) {
const region = row.express_region || '';
const addr = row.express_address || '';
return `${region}${addr}`.trim() || '—';
}
onMounted(() => {
void loadList();
});
</script>
<template>
<Page auto-content-height title="我的订单">
<FormModal />
<DetailModal />
<div class="flex flex-col gap-4">
<Tabs
:active-key="statusTab"
type="card"
@change="handleTabChange"
>
<Tabs.TabPane key="1" tab="待发货" />
<Tabs.TabPane key="2" tab="待收货" />
<Tabs.TabPane key="7" tab="确认收货" />
</Tabs>
<div class="flex flex-wrap items-center gap-3">
<Input
v-model:value="orderNo"
allow-clear
class="w-64"
placeholder="输入订单号"
@press-enter="handleSearch"
/>
<Button type="primary" @click="handleSearch">查询</Button>
<span class="text-sm text-[hsl(var(--muted-foreground))]">
当前{{ tabLabel }}
</span>
</div>
<Spin :spinning="loading">
<div
v-if="list.length"
class="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3"
>
<Card
v-for="row in list"
:key="row.id"
class="overflow-hidden"
size="small"
>
<template #title>
<div class="flex flex-wrap items-center gap-2">
<span class="font-medium">{{ row.order_no || '—' }}</span>
<Tag :color="statusMeta[row.status]?.color || 'default'">
{{ statusMeta[row.status]?.text || row.status }}
</Tag>
</div>
</template>
<template #extra>
<span class="text-xs text-[hsl(var(--muted-foreground))]">
{{ row.created_at || '' }}
</span>
</template>
<div class="space-y-2 text-sm">
<div>
<span class="text-[hsl(var(--muted-foreground))]">收件人</span>
{{ row.express_name || '—' }}
<span class="ml-2">{{ row.express_mobile || '' }}</span>
</div>
<div>
<span class="text-[hsl(var(--muted-foreground))]">地址</span>
{{ addressText(row) }}
</div>
<div>
<div class="mb-1 text-[hsl(var(--muted-foreground))]">
本仓商品
</div>
<div
v-if="productRows(row).length"
class="flex flex-col gap-2"
>
<div
v-for="p in productRows(row)"
:key="p.id"
class="flex items-center gap-2"
>
<Image
:src="p.drug?.image || p.drug_image || ''"
:width="40"
:height="40"
class="shrink-0 rounded object-cover"
fallback="/img/empty.png"
/>
<div class="min-w-0 flex-1 text-xs">
<div class="truncate font-medium">
{{ p.drug_name || p.drug?.drug_name || '-' }}
</div>
<div class="text-[hsl(var(--muted-foreground))]">
规格{{ p.drug?.specification || '-' }}
· 数量{{ qtyText(p) }}
</div>
</div>
</div>
</div>
<span v-else></span>
</div>
<div>
<span class="text-[hsl(var(--muted-foreground))]">物流</span>
<span
:class="
row.logistics_summary === '未发货'
? 'text-[hsl(var(--muted-foreground))]'
: ''
"
>
{{ row.logistics_summary || '未发货' }}
</span>
</div>
</div>
<template #actions>
<Space>
<Button size="small" type="link" @click="openDetail(row, false)">
详情
</Button>
<Button
v-if="
Number(row.is_send) === 1 ||
Number(row.status) >= 2 ||
!!row.waybill_no
"
size="small"
type="link"
@click="openDetail(row, true)"
>
物流
</Button>
<Button
v-if="Number(row.status) === 1"
size="small"
type="link"
@click="wareSend(row)"
>
发货
</Button>
</Space>
</template>
</Card>
</div>
<Empty v-else class="py-16" :description="`${tabLabel}暂无订单`" />
</Spin>
<div v-if="pager.total > 0" class="flex justify-end">
<Pagination
v-model:current="pager.currentPage"
v-model:page-size="pager.pageSize"
:total="pager.total"
show-size-changer
:page-size-options="['12', '24', '48']"
@change="loadList"
@show-size-change="loadList"
/>
</div>
</div>
</Page>
</template>

View File

@@ -0,0 +1,6 @@
export {
getDeliveryWarehouseDrugList,
getDeliveryWarehouseStockLogList,
stockInDeliveryWarehouseDrug,
stockOutDeliveryWarehouseDrug,
} from '#/views/system/delivery-warehouse-drug/api';

View File

@@ -0,0 +1,21 @@
import type { VbenFormProps } from '#/adapter/form';
export const formOptions: VbenFormProps = {
collapsed: false,
schema: [
{
component: 'VbenInput',
componentProps: {
placeholder: '输入药品名称',
},
fieldName: 'drug_name',
label: '药品名称',
},
],
showCollapseButton: false,
submitButtonOptions: {
content: '查询',
},
submitOnChange: true,
submitOnEnter: false,
};

View File

@@ -0,0 +1,67 @@
import type { VxeGridProps } from '#/adapter/vxe-table';
import { getDeliveryWarehouseDrugList } from '#/views/system/delivery-warehouse-stock/api';
interface RowType {
id: number;
drug_name: string;
quote: string;
promo_fee: string;
platform_fee: string;
stock: number;
frozen_number: number;
drug?: { drug_name: string };
}
export const gridOptions: VxeGridProps<RowType> = {
columnConfig: {
useKey: true,
},
rowConfig: {
useKey: true,
},
columns: [
{
field: 'drug_name',
title: '药品名称',
slots: { default: 'drug_name' },
minWidth: 160,
},
{ field: 'quote', title: '报价' },
{ field: 'promo_fee', title: '推广费' },
{ field: 'platform_fee', title: '平台费' },
{ field: 'stock', title: '库存' },
{ field: 'frozen_number', title: '冻结数量' },
{ type: 'html', title: '操作', slots: { default: 'action' }, width: 100 },
],
keepSource: true,
pagerConfig: {},
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getDeliveryWarehouseDrugList({
page: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
height: 'auto',
border: false,
toolbarConfig: {
// @ts-ignore
search: true,
refresh: true,
print: false,
export: false,
zoom: true,
slots: {
buttons: 'toolbar-buttons',
},
custom: {
icon: 'vxe-icon-menu',
},
},
showOverflow: false,
};

View File

@@ -0,0 +1,212 @@
<script lang="ts" setup>
import { onMounted, reactive, ref } from 'vue';
import { Page, useVbenDrawer, useVbenModal } from '@vben/common-ui';
import {
Button,
Card,
Empty,
Input,
Pagination,
Space,
Spin,
Tag,
} from 'ant-design-vue';
import { getDeliveryWarehouseDrugList } from '#/views/system/delivery-warehouse-stock/api';
import StockIoModal from './components/stock-io-modal.vue';
import StockLogDrawer from './components/stock-log-drawer.vue';
defineOptions({ name: 'DeliveryWarehouseStockIndex' });
const loading = ref(false);
const keyword = ref('');
const list = ref<Record<string, any>[]>([]);
const pager = reactive({
currentPage: 1,
pageSize: 12,
total: 0,
});
const [IoModal, ioModalApi] = useVbenModal({
connectedComponent: StockIoModal,
});
const [LogDrawer, logDrawerApi] = useVbenDrawer({
connectedComponent: StockLogDrawer,
});
/**
* 加载库存卡片(绑药列表)
*/
async function loadList() {
loading.value = true;
try {
const res = await getDeliveryWarehouseDrugList({
page: pager.currentPage,
pageSize: pager.pageSize,
// 后端无药名 like 时仅本地过滤;有则透传
drug_name: keyword.value?.trim() || undefined,
});
let items = res?.items ?? res?.list ?? [];
const kw = keyword.value?.trim();
if (kw) {
items = items.filter((row: any) => {
const name = String(row.drug?.drug_name || '');
const no = String(row.drug?.drug_number || '');
return name.includes(kw) || no.includes(kw);
});
}
list.value = items;
pager.total = Number(res?.total ?? 0);
} finally {
loading.value = false;
}
}
function openIo(row: Record<string, any>, mode: 'in' | 'out') {
ioModalApi.setData({
mode,
row,
onSuccess: () => loadList(),
});
ioModalApi.open();
}
function openLogs(row: Record<string, any>) {
logDrawerApi.setData({ row });
logDrawerApi.open();
}
function available(row: Record<string, any>) {
if (row.available_stock !== undefined && row.available_stock !== null) {
return Number(row.available_stock);
}
return Math.max(Number(row.stock || 0) - Number(row.frozen_number || 0), 0);
}
onMounted(() => {
void loadList();
});
</script>
<template>
<Page auto-content-height title="库存管理">
<IoModal />
<LogDrawer />
<div class="flex flex-col gap-4">
<div class="flex flex-wrap items-center gap-3">
<Input
v-model:value="keyword"
allow-clear
class="w-64"
placeholder="药品名称/编号"
@press-enter="() => { pager.currentPage = 1; void loadList(); }"
/>
<Button
type="primary"
@click="
() => {
pager.currentPage = 1;
void loadList();
}
"
>
查询
</Button>
<span class="text-sm text-[hsl(var(--muted-foreground))]">
库存变动请用入库/出库,禁止直接改数量
</span>
</div>
<Spin :spinning="loading">
<div
v-if="list.length"
class="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3"
>
<Card
v-for="row in list"
:key="row.id"
size="small"
>
<template #title>
<div class="flex flex-col gap-1">
<span>{{ row.drug?.drug_name || '-' }}</span>
<span class="text-xs font-normal text-[hsl(var(--muted-foreground))]">
规格:{{ row.drug?.specification?.trim() || '-' }}
</span>
<span class="text-xs font-normal text-[hsl(var(--muted-foreground))]">
{{ row.drug?.drug_number || '-' }}
<template v-if="row.warehouse?.name">
· {{ row.warehouse.name }}
</template>
</span>
</div>
</template>
<template #extra>
<Tag :color="Number(row.status) === 2 ? 'green' : 'default'">
{{ Number(row.status) === 2 ? '上架' : '下架' }}
</Tag>
</template>
<div class="grid grid-cols-3 gap-2 text-center text-sm">
<div class="rounded bg-[hsl(var(--muted)/0.35)] p-2">
<div class="text-lg font-semibold text-emerald-600">
{{ available(row) }}
</div>
<div class="text-xs text-[hsl(var(--muted-foreground))]">
可用
</div>
</div>
<div class="rounded bg-[hsl(var(--muted)/0.35)] p-2">
<div class="text-lg font-semibold text-orange-500">
{{ row.frozen_number ?? 0 }}
</div>
<div class="text-xs text-[hsl(var(--muted-foreground))]">
冻结
</div>
</div>
<div class="rounded bg-[hsl(var(--muted)/0.35)] p-2">
<div class="text-lg font-semibold">
{{ row.stock ?? 0 }}
</div>
<div class="text-xs text-[hsl(var(--muted-foreground))]">
总库存
</div>
</div>
</div>
<template #actions>
<Space wrap>
<Button size="small" type="link" @click="openIo(row, 'in')">
入库
</Button>
<Button size="small" type="link" @click="openIo(row, 'out')">
出库
</Button>
<Button size="small" type="link" @click="openLogs(row)">
出入库记录
</Button>
</Space>
</template>
</Card>
</div>
<Empty v-else class="py-16" description="暂无库存药品" />
</Spin>
<div v-if="pager.total > 0" class="flex justify-end">
<Pagination
v-model:current="pager.currentPage"
v-model:page-size="pager.pageSize"
:total="pager.total"
show-size-changer
:page-size-options="['12', '24', '48']"
@change="loadList"
@show-size-change="loadList"
/>
</div>
</div>
</Page>
</template>

View File

@@ -0,0 +1,17 @@
import { requestClient } from '#/api/request';
const prefix = 'withdrawal-application/';
/**
* 提现申请列表
*/
export async function getWithdrawalApplicationList(data: any = {}) {
return requestClient.get<any>(`${prefix}list`, { params: data });
}
/**
* 申请提现(后端方法名为 withdrawal
*/
export async function applyWithdrawal(data: { amount: number }) {
return requestClient.post<any>(`${prefix}withdrawal`, data);
}

View File

@@ -0,0 +1,27 @@
import type { VbenFormProps } from '#/adapter/form';
export const applyFormProps: VbenFormProps = {
wrapperClass: 'grid-cols-12',
commonConfig: {
formItemClass: 'col-span-12',
componentProps: {
class: 'w-full',
},
},
layout: 'horizontal',
schema: [
{
component: 'InputNumber',
componentProps: {
placeholder: '请输入提现金额',
min: 1,
precision: 2,
class: 'w-full',
},
fieldName: 'amount',
label: '提现金额',
rules: 'required',
},
],
showDefaultActions: false,
};

View File

@@ -0,0 +1,72 @@
import type { VxeGridProps } from '#/adapter/vxe-table';
import { getWithdrawalApplicationList } from '#/views/system/delivery-warehouse-withdrawal/api';
interface RowType {
id: number;
apply_cash: number;
true_cash: number;
charge_cash: number;
apply_time: string;
check_status: number;
check_result: string;
dakuan_status: number;
dakuan_time: string;
created_at: string;
}
export const gridOptions: VxeGridProps<RowType> = {
columnConfig: {
useKey: true,
},
rowConfig: {
useKey: true,
},
columns: [
{ field: 'id', align: 'left', title: 'ID', width: 80 },
{ field: 'apply_cash', title: '申请金额' },
{ field: 'true_cash', title: '实际到账' },
{ field: 'charge_cash', title: '手续费' },
{ field: 'apply_time', title: '申请时间' },
{
field: 'check_status',
title: '审核状态',
slots: { default: 'check_status' },
},
{
field: 'check_result',
title: '审核结果',
slots: { default: 'check_result' },
},
{
field: 'dakuan_status',
title: '打款状态',
slots: { default: 'dakuan_status' },
},
{ field: 'dakuan_time', title: '打款时间' },
],
keepSource: true,
pagerConfig: {},
proxyConfig: {
ajax: {
query: async ({ page }) => {
return await getWithdrawalApplicationList({
page: page.currentPage,
pageSize: page.pageSize,
});
},
},
},
height: 'auto',
border: false,
toolbarConfig: {
refresh: true,
print: false,
export: false,
zoom: true,
custom: {
icon: 'vxe-icon-menu',
},
},
showOverflow: false,
};

View File

@@ -0,0 +1,31 @@
<script lang="ts" setup>
/**
* 已废弃:仓侧提现与财务中心共用 views/finance/withdrawal/index
* 菜单 225 已指向共用页;本文件仅作兼容重定向,避免旧路由 404
*/
import { onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { Page } from '@vben/common-ui';
const router = useRouter();
onMounted(() => {
// 优先跳到仓菜单配置的提现路径;找不到则落到财务提现
const routes = router.getRoutes();
const hit = routes.find(
(r) =>
typeof r.path === 'string' &&
r.path.includes('delivery-warehouse') &&
r.path.includes('withdrawal') &&
!r.path.includes('delivery-warehouse-withdrawal'),
);
router.replace(hit?.path || '/finance/withdrawal');
});
</script>
<template>
<Page title="提现申请">
<div class="text-[hsl(var(--muted-foreground))]">正在跳转至提现管理</div>
</Page>
</template>

View File

@@ -0,0 +1,45 @@
import { requestClient } from '#/api/request';
const prefix = 'delivery-warehouse/';
/**
* 分页查询配送仓库列表
*/
export async function getDeliveryWarehouseList(data: any) {
return requestClient.get<any>(`${prefix}list`, { params: data });
}
/**
* 配送仓库下拉选项
*/
export async function getDeliveryWarehouseOption(data: any = {}) {
return requestClient.get<any>(`${prefix}option`, { params: data });
}
/**
* 获取配送仓库详情
*/
export async function getDeliveryWarehouseInfo(id: number) {
return requestClient.get<any>(`${prefix}detail`, { params: { id } });
}
/**
* 新增配送仓库
*/
export async function createDeliveryWarehouse(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}create`, data);
}
/**
* 编辑配送仓库
*/
export async function updateDeliveryWarehouse(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}update`, data);
}
/**
* 删除配送仓库
*/
export async function deleteDeliveryWarehouse(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}delete`, data);
}

View File

@@ -0,0 +1,90 @@
<script lang="ts" setup>
/**
* 平台仓库列表:银行卡报备弹窗
* 复用仓侧报备表单与 API提交时带当前行 warehouse_id
*/
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
getDeliveryWarehouseBankCardDetail,
reportDeliveryWarehouseBankCard,
} from '#/views/system/delivery-warehouse-bank-card/api';
import { reportFormProps } from '#/views/system/delivery-warehouse-bank-card/config/form';
const warehouseId = ref(0);
const warehouseName = ref('');
const gridApi = ref();
const [Form, formApi] = useVbenForm(reportFormProps);
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
draggable: true,
onCancel() {
modalApi.close();
},
onConfirm: async () => {
const result = await formApi.validate();
if (!result.valid) {
return;
}
const values = await formApi.getValues();
modalApi.setState({ loading: true, confirmLoading: true });
try {
await reportDeliveryWarehouseBankCard({
...values,
warehouse_id: warehouseId.value,
});
message.success('银行卡报备已提交');
gridApi.value?.query?.() ?? gridApi.value?.reload?.();
modalApi.close();
} finally {
modalApi.setState({ loading: false, confirmLoading: false });
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
return;
}
const data = modalApi.getData<Record<string, any>>() || {};
warehouseId.value = Number(data.warehouse_id || data.values?.id || 0);
warehouseName.value = data.warehouse_name || data.values?.name || '';
gridApi.value = data.gridApi;
formApi.resetForm();
formApi.setValues({ warehouse_id: warehouseId.value });
// 有历史草稿或主体镜像时预填,方便驳回后重提
try {
const detail = await getDeliveryWarehouseBankCardDetail({
warehouse_id: warehouseId.value,
});
if (detail) {
formApi.setValues({
warehouse_id: warehouseId.value,
bank_user_name: detail.bank_user_name,
bank_card: detail.bank_card,
bank_name: detail.bank_name,
bank_account_type: detail.bank_account_type || 2,
bank_no: detail.bank_no,
cert_no: detail.cert_no,
attachment: detail.attachment,
});
}
} catch {
// 未报备时 detail 可能为空,忽略即可
}
},
});
</script>
<template>
<Modal
:title="`银行卡报备${warehouseName ? ` - ${warehouseName}` : ''}`"
class="w-[520px]"
>
<Form />
</Modal>
</template>

View File

@@ -0,0 +1,126 @@
<script lang="ts" setup>
/**
* 平台仓库列表:银行卡报备状态查询弹窗
* 展示最新报备信息,支持同步易票联状态
*/
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Button, Descriptions, message, Tag } from 'ant-design-vue';
import {
getDeliveryWarehouseBankCardDetail,
syncDeliveryWarehouseBankCardStatus,
} from '#/views/system/delivery-warehouse-bank-card/api';
const warehouseId = ref(0);
const warehouseName = ref('');
const detail = ref<Record<string, any>>({});
const loading = ref(false);
const syncing = ref(false);
const gridApi = ref();
const reportStatusMap: Record<number, { text: string; color: string }> = {
1: { text: '待审核', color: 'orange' },
2: { text: '有效', color: 'green' },
3: { text: '审核不通过', color: 'red' },
4: { text: '无效', color: 'default' },
};
const bankAccountTypeMap: Record<number, string> = {
1: '对公',
2: '对私',
5: '存折',
};
async function loadDetail() {
if (!warehouseId.value) {
return;
}
loading.value = true;
try {
detail.value =
(await getDeliveryWarehouseBankCardDetail({
warehouse_id: warehouseId.value,
})) || {};
} finally {
loading.value = false;
}
}
async function handleSyncStatus() {
syncing.value = true;
try {
await syncDeliveryWarehouseBankCardStatus({
warehouse_id: warehouseId.value,
});
message.success('状态已同步');
await loadDetail();
gridApi.value?.query?.() ?? gridApi.value?.reload?.();
} finally {
syncing.value = false;
}
}
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
draggable: true,
showConfirmButton: false,
onCancel() {
modalApi.close();
},
onOpenChange(isOpen: boolean) {
if (!isOpen) {
return;
}
const data = modalApi.getData<Record<string, any>>() || {};
warehouseId.value = Number(data.warehouse_id || data.values?.id || 0);
warehouseName.value = data.warehouse_name || data.values?.name || '';
gridApi.value = data.gridApi;
loadDetail();
},
});
</script>
<template>
<Modal
:title="`报备状态${warehouseName ? ` - ${warehouseName}` : ''}`"
class="w-[480px]"
>
<Descriptions :column="1" bordered size="small" :loading="loading">
<Descriptions.Item label="报备状态">
<Tag
v-if="detail.report_status"
:color="reportStatusMap[detail.report_status]?.color"
>
{{
detail.report_status_text ||
reportStatusMap[detail.report_status]?.text ||
'-'
}}
</Tag>
<span v-else>未报备</span>
</Descriptions.Item>
<Descriptions.Item label="审核意见">
{{ detail.audit_remarks || detail.report_status_msg || '-' }}
</Descriptions.Item>
<Descriptions.Item label="开户人">
{{ detail.bank_user_name || '-' }}
</Descriptions.Item>
<Descriptions.Item label="银行卡号">
{{ detail.bank_card_masked || detail.bank_card || '-' }}
</Descriptions.Item>
<Descriptions.Item label="开户行">
{{ detail.bank_name || '-' }}
</Descriptions.Item>
<Descriptions.Item label="账户类型">
{{ bankAccountTypeMap[detail.bank_account_type] || '-' }}
</Descriptions.Item>
</Descriptions>
<div class="mt-4">
<Button :loading="syncing" type="primary" @click="handleSyncStatus">
查询/同步状态
</Button>
</div>
</Modal>
</template>

View File

@@ -0,0 +1,67 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
createDeliveryWarehouse,
updateDeliveryWarehouse,
} from '#/views/system/delivery-warehouse/api';
import { modalFormProps } from '#/views/system/delivery-warehouse/config/form';
const isUpdate = ref(false);
const gridApi = ref();
const [Form, formApi] = useVbenForm(modalFormProps);
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
draggable: true,
onCancel() {
modalApi.close();
},
onConfirm: async () => {
formApi.validate().then(async (e: any) => {
if (e.valid) {
const values = await formApi.getValues();
modalApi.setState({ loading: true, confirmLoading: true });
const submitApi = isUpdate.value
? updateDeliveryWarehouse
: createDeliveryWarehouse;
submitApi(values)
.then(() => {
message.success('保存成功');
// 优先 query兼容部分页面挂的 reload
gridApi.value?.query?.() ?? gridApi.value?.reload?.();
modalApi.close();
})
.finally(() => {
modalApi.setState({ loading: false, confirmLoading: false });
});
}
});
await formApi.validateAndSubmitForm();
},
onOpenChange(isOpen: boolean) {
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
if (isOpen) {
const { values, update } = modalApi.getData<Record<string, any>>();
if (values) {
isUpdate.value = update;
formApi.setValues(values);
}
}
},
});
</script>
<template>
<Modal
:title="`${isUpdate === true ? '编辑' : '新增'}配送仓库`"
class="w-[40%]"
>
<Form />
</Modal>
</template>

View File

@@ -0,0 +1,59 @@
import type { VbenFormProps } from '#/adapter/form';
/**
* 配送仓库新建/编辑:仅基本信息
* 合同与银行卡走独立报备入口,不在此表单填写
*/
export const modalFormProps: VbenFormProps = {
wrapperClass: 'grid-cols-12',
commonConfig: {
formItemClass: 'col-span-12',
componentProps: {
class: 'w-full',
},
},
layout: 'horizontal',
schema: [
{
component: 'VbenInput',
fieldName: 'id',
label: 'ID',
formItemClass: 'col-span-6',
dependencies: {
show: false,
triggerFields: ['id'],
},
},
{
component: 'VbenInput',
componentProps: {
placeholder: '请输入配送仓库名称',
},
fieldName: 'name',
label: '仓库名称',
rules: 'required',
},
{
component: 'Textarea',
componentProps: {
placeholder: '请输入仓库介绍(选填)',
},
fieldName: 'introduce',
label: '仓库介绍',
},
{
component: 'RadioGroup',
componentProps: {
options: [
{ label: '禁用', value: 0 },
{ label: '启用', value: 1 },
],
},
fieldName: 'status',
label: '状态',
defaultValue: 1,
formItemClass: 'col-span-6',
},
],
showDefaultActions: false,
};

View File

@@ -0,0 +1,22 @@
import type { VbenFormProps } from '#/adapter/form';
export const formOptions: VbenFormProps = {
collapsed: false,
schema: [
{
component: 'VbenInput',
componentProps: {
placeholder: '输入名称',
},
defaultValue: '',
fieldName: 'name',
label: '仓库名称',
},
],
showCollapseButton: true,
submitButtonOptions: {
content: '查询',
},
submitOnChange: true,
submitOnEnter: false,
};

View File

@@ -0,0 +1,76 @@
import type { VxeGridProps } from '#/adapter/vxe-table';
import { getDeliveryWarehouseList } from '#/views/system/delivery-warehouse/api';
interface RowType {
id: string;
name: string;
introduce: string;
status: number;
bank_card_report_status?: number;
bank_card_report_status_text?: string;
created_at: string;
}
export const gridOptions: VxeGridProps<RowType> = {
checkboxConfig: {
highlight: true,
labelField: '',
},
columnConfig: {
useKey: true,
},
rowConfig: {
useKey: true,
},
columns: [
{ type: 'checkbox', width: 60 },
{ field: 'id', align: 'left', title: 'ID', width: 100 },
{ field: 'name', align: 'left', title: '仓库名称' },
{ field: 'introduce', title: '仓库介绍' },
{
field: 'bank_card_report_status',
title: '银行卡报备',
slots: { default: 'bank_card_report_status' },
width: 120,
},
{
field: 'status',
title: '状态',
slots: { default: 'status' },
width: 100,
},
{ field: 'created_at', title: '创建时间' },
{ type: 'html', title: '操作', slots: { default: 'action' }, width: 260 },
],
keepSource: true,
pagerConfig: {},
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getDeliveryWarehouseList({
page: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
height: 'auto',
border: false,
toolbarConfig: {
// @ts-ignore
search: true,
refresh: true,
print: false,
export: false,
zoom: true,
slots: {
buttons: 'toolbar-buttons',
},
custom: {
icon: 'vxe-icon-menu',
},
},
showOverflow: false,
};

View File

@@ -0,0 +1,234 @@
<script lang="ts" setup>
/**
* 平台配送仓库列表
* 新建/编辑仅基本信息;银行卡报备对齐诊所,走独立弹窗
*/
import type { VxeGridListeners } from '#/adapter/vxe-table';
import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { Button, message, Tag } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { TableAction } from '#/components/table-action';
import { deleteDeliveryWarehouse } from './api';
import BankCardReportModal from './components/bank-card-report-modal.vue';
import BankCardStatusModal from './components/bank-card-status-modal.vue';
import FormModalDemo from './components/modal.vue';
import { formOptions } from './config/search';
import { gridOptions } from './config/table';
const hasTopTableDropDownActions = ref(false);
const reportStatusColorMap: Record<number, string> = {
0: 'default',
1: 'orange',
2: 'green',
3: 'red',
4: 'default',
};
const gridEvents: VxeGridListeners<any> = {
checkboxChange() {
const records = gridApi.grid.getCheckboxRecords();
hasTopTableDropDownActions.value = records.length > 0;
},
checkboxAll() {
const records = gridApi.grid.getCheckboxRecords();
hasTopTableDropDownActions.value = records.length > 0;
},
};
const [Grid, gridApi] = useVbenVxeGrid({
formOptions,
gridOptions,
gridEvents,
});
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: FormModalDemo,
});
const [BankCardReportModalComponent, bankCardReportModalApi] = useVbenModal({
connectedComponent: BankCardReportModal,
});
const [BankCardStatusModalComponent, bankCardStatusModalApi] = useVbenModal({
connectedComponent: BankCardStatusModal,
});
const showModal = (data = {}, isUpdate = false) => {
formModalApi.setData({
values: data,
update: isUpdate,
gridApi,
});
formModalApi.open();
};
/**
* 未报备/驳回:打开报备弹窗;待审核/有效:打开状态弹窗
*/
function handleBankCardReportClick(row: any) {
const status = Number(row.bank_card_report_status ?? 0);
if (status === 1 || status === 2) {
bankCardStatusModalApi.setData({
warehouse_id: row.id,
warehouse_name: row.name,
values: row,
gridApi,
});
bankCardStatusModalApi.open();
return;
}
bankCardReportModalApi.setData({
warehouse_id: row.id,
warehouse_name: row.name,
values: row,
gridApi,
});
bankCardReportModalApi.open();
}
function openBankCardStatus(row: any) {
bankCardStatusModalApi.setData({
warehouse_id: row.id,
warehouse_name: row.name,
values: row,
gridApi,
});
bankCardStatusModalApi.open();
}
const deleteApi = (row: any) => {
let ids = [];
if (row) {
ids.push(row);
} else {
ids = gridApi.grid.getCheckboxRecords().map((item) => item.id);
}
deleteDeliveryWarehouse({ ids }).then(() => {
message.success('删除成功!');
gridApi.query();
});
};
</script>
<template>
<Page auto-content-height title="配送仓库管理">
<FormModal />
<BankCardReportModalComponent />
<BankCardStatusModalComponent />
<Grid>
<template #toolbar-buttons>
<TableAction
:actions="[
{
label: '新增',
type: 'primary',
icon: 'ant-design:plus-outlined',
onClick: showModal.bind(null),
},
]"
:drop-down-actions="[
{
label: '删除',
icon: 'ant-design:delete-outlined',
ifShow: hasTopTableDropDownActions,
popConfirm: {
title: '确定删除吗',
confirm: deleteApi.bind(null, false),
},
},
]"
>
<template #more>
<Button style="margin-left: 16px">
批量操作
<Icon icon="ant-design:down-outlined" />
</Button>
</template>
</TableAction>
</template>
<template #bank_card_report_status="{ row }">
<Tag
:color="
reportStatusColorMap[Number(row.bank_card_report_status ?? 0)] ||
'default'
"
class="cursor-pointer"
@click="handleBankCardReportClick(row)"
>
{{ row.bank_card_report_status_text || '未报备' }}
</Tag>
</template>
<template #status="{ row }">
<Tag v-if="row.status === 1" color="green">启用</Tag>
<Tag v-else color="default">禁用</Tag>
</template>
<template #toolbar-tools></template>
<template #action="{ row }">
<TableAction
:actions="[
{
label: '编辑',
type: 'link',
icon: 'uil:edit',
size: 'small',
onClick: showModal.bind(null, row, true),
},
{
label:
Number(row.bank_card_report_status ?? 0) === 1 ||
Number(row.bank_card_report_status ?? 0) === 2
? '报备状态'
: '银行卡报备',
type: 'link',
icon: 'ant-design:credit-card-outlined',
size: 'small',
onClick: handleBankCardReportClick.bind(null, row),
},
{
label: '删除',
type: 'link',
icon: 'ant-design:delete-outlined',
size: 'small',
popConfirm: {
title: '确定删除吗?',
confirm: deleteApi.bind(null, row.id),
},
},
]"
:drop-down-actions="
Number(row.bank_card_report_status ?? 0) === 1 ||
Number(row.bank_card_report_status ?? 0) === 2
? [
{
label: '重新报备',
icon: 'ant-design:reload-outlined',
onClick: () => {
bankCardReportModalApi.setData({
warehouse_id: row.id,
warehouse_name: row.name,
values: row,
gridApi,
});
bankCardReportModalApi.open();
},
},
]
: [
{
label: '查看报备',
icon: 'ant-design:eye-outlined',
onClick: openBankCardStatus.bind(null, row),
},
]
"
/>
</template>
</Grid>
</Page>
</template>