1. 修复了接诊页面的剂量、天数输入框过窄
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: '密码',
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 配送仓管理员账号列表(角色 16)
|
||||
*/
|
||||
import AdminPage from '../_shared/admin-page.vue';
|
||||
</script>
|
||||
<template>
|
||||
<AdminPage :role-id="16" />
|
||||
</template>
|
||||
10
apps/web-antd/src/views/system/admin/order/index.vue
Normal file
10
apps/web-antd/src/views/system/admin/order/index.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 订单管理员账号列表(角色 15)
|
||||
* 本阶段仅账号入口,业务菜单权限后续再挂
|
||||
*/
|
||||
import AdminPage from '../_shared/admin-page.vue';
|
||||
</script>
|
||||
<template>
|
||||
<AdminPage :role-id="15" />
|
||||
</template>
|
||||
@@ -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 || {});
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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>
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
135
apps/web-antd/src/views/system/delivery-warehouse-drug/index.vue
Normal file
135
apps/web-antd/src/views/system/delivery-warehouse-drug/index.vue
Normal 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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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>
|
||||
@@ -0,0 +1,6 @@
|
||||
export {
|
||||
getDeliveryWarehouseDrugList,
|
||||
getDeliveryWarehouseStockLogList,
|
||||
stockInDeliveryWarehouseDrug,
|
||||
stockOutDeliveryWarehouseDrug,
|
||||
} from '#/views/system/delivery-warehouse-drug/api';
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
234
apps/web-antd/src/views/system/delivery-warehouse/index.vue
Normal file
234
apps/web-antd/src/views/system/delivery-warehouse/index.vue
Normal 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>
|
||||
Reference in New Issue
Block a user