fix: 接诊UI

This commit is contained in:
李琦
2026-08-07 20:24:49 +08:00
parent 33dcd3f5d8
commit 7a504433da
32 changed files with 2638 additions and 297 deletions

View File

@@ -237,7 +237,7 @@ async function onSelectDrug(item: any) {
class="w-[40%]"
>
<div class="mb-4">
<div class="mb-1 text-sm text-gray-600">
<div class="mb-1 text-sm text-muted-foreground">
药品
<span class="text-red-500">*</span>
</div>

View File

@@ -0,0 +1,30 @@
import { requestClient } from '#/api/request';
const prefix = 'delivery-warehouse-product/';
/**
* 平台仓品审核列表
* audit_status 为空时后端默认只返回待审
*/
export async function getDeliveryWarehouseProductAuditList(data: any) {
return requestClient.get<any>(`${prefix}audit-list`, { params: data });
}
/**
* 仓品详情(审核弹窗展示)
*/
export async function getDeliveryWarehouseProductInfo(id: number) {
return requestClient.get<any>(`${prefix}detail`, { params: { id } });
}
/**
* 平台审核:status 必为 2 下架或 3 上架;需填 zone_id
*/
export async function auditDeliveryWarehouseProduct(data: {
id: number;
status: 2 | 3;
zone_id: number;
category_id?: number;
}) {
return requestClient.post<any>(`${prefix}audit`, data);
}

View File

@@ -0,0 +1,200 @@
<script lang="ts" setup>
/**
* 平台仓品审核弹窗
* 必选上架/下架 + 所属分区;审核成功后由列表页打开仓药绑定弹窗
*/
import { nextTick, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Descriptions, Image, message, Tag } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { getDrugCategoriesTree, getZoneOptions } from '#/views/business/product/drug-categories/api';
import {
auditDeliveryWarehouseProduct,
getDeliveryWarehouseProductInfo,
} from '../api';
const gridApi = ref();
const detail = ref<Record<string, any> | null>(null);
/** 审核成功后回传给列表页,用于打开绑仓弹窗 */
const onBindPreset = ref<((preset: Record<string, any>) => void) | null>(null);
const [Form, formApi] = useVbenForm({
wrapperClass: 'grid-cols-12',
commonConfig: {
formItemClass: 'col-span-12',
componentProps: {
class: 'w-full',
},
},
layout: 'horizontal',
schema: [
{
component: 'VbenInput',
fieldName: 'id',
label: 'ID',
dependencies: {
show: false,
triggerFields: ['id'],
},
},
{
component: 'ApiSelect',
componentProps: {
api: getZoneOptions,
labelField: 'title',
valueField: 'id',
placeholder: '请选择分区',
allowClear: false,
},
fieldName: 'zone_id',
label: '所属分区',
rules: 'selectRequired',
},
{
component: 'ApiTreeSelect',
componentProps: {
api: getDrugCategoriesTree,
childrenField: 'children',
labelField: 'category_name',
valueField: 'id',
placeholder: '请先选择分区',
allowClear: true,
treeDefaultExpandAll: true,
immediate: false,
alwaysLoad: true,
},
fieldName: 'category_id',
label: '所属分类',
dependencies: {
componentProps(values, formApiInst) {
return {
beforeFetch: async () => {
const currentValues = await formApiInst.getValues();
return { zone_id: currentValues.zone_id };
},
placeholder: values.zone_id ? '请选择分类' : '请先选择分区',
};
},
trigger(_values, formApiInst) {
formApiInst.setFieldValue('category_id', undefined);
},
triggerFields: ['zone_id'],
},
},
{
component: 'RadioGroup',
componentProps: {
options: [
{ label: '上架', value: 3 },
{ label: '下架', value: 2 },
],
},
fieldName: 'status',
label: '审核结果',
rules: 'selectRequired',
help: '审核通过后按所选结果上架或下架,并打开绑仓弹窗',
},
],
showDefaultActions: false,
});
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();
const status = Number(values.status);
if (status !== 2 && status !== 3) {
message.warning('请选择上架或下架');
return;
}
if (!values.zone_id) {
message.warning('请选择所属分区');
return;
}
modalApi.setState({ loading: true, confirmLoading: true });
auditDeliveryWarehouseProduct({
id: Number(values.id),
status: status as 2 | 3,
zone_id: Number(values.zone_id),
category_id: Number(values.category_id || 0),
})
.then((res: any) => {
message.success('审核成功,请完成仓药绑定');
gridApi.value?.query?.() ?? gridApi.value?.reload?.();
const preset = res?.bind_preset || res?.data?.bind_preset;
modalApi.close();
if (preset && onBindPreset.value) {
nextTick(() => onBindPreset.value?.(preset));
}
})
.finally(() => {
modalApi.setState({ loading: false, confirmLoading: false });
});
}
});
await formApi.validateAndSubmitForm();
},
async onOpenChange(isOpen: boolean) {
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
onBindPreset.value = isOpen ? modalApi.getData()?.onBindPreset || null : null;
if (!isOpen) {
detail.value = null;
return;
}
await formApi.resetForm();
const data = modalApi.getData<Record<string, any>>() || {};
const id = Number(data.id || data.values?.id || 0);
await formApi.setValues({ id, status: undefined, zone_id: undefined, category_id: undefined });
if (id > 0) {
const row = data.values || { id };
try {
const info = await getDeliveryWarehouseProductInfo(id);
detail.value = { ...row, ...(info || {}) };
} catch {
detail.value = row;
}
}
},
});
</script>
<template>
<Modal title="仓品审核" class="w-[80%] md:w-[50%] lg:w-[40%]">
<div v-if="detail" class="mb-4">
<Descriptions :column="1" size="small" bordered>
<Descriptions.Item label="药品名称">
{{ detail.drug_name || '-' }}
</Descriptions.Item>
<Descriptions.Item label="规格">
{{ detail.specification || '-' }}
</Descriptions.Item>
<Descriptions.Item label="类型">
{{ detail.type_txt || '-' }}
</Descriptions.Item>
<Descriptions.Item label="上传报价">
¥{{ detail.upload_quote || '0' }}
</Descriptions.Item>
<Descriptions.Item label="当前状态">
<Tag>{{ detail.status_txt || '-' }}</Tag>
<Tag color="orange" class="ml-1">
{{ detail.audit_status_txt || '待审核' }}
</Tag>
</Descriptions.Item>
<Descriptions.Item v-if="detail.image" label="产品图">
<Image :src="detail.image" :height="48" :width="48" />
</Descriptions.Item>
</Descriptions>
</div>
<Form />
</Modal>
</template>

View File

@@ -0,0 +1,41 @@
import type { VbenFormProps } from '#/adapter/form';
/**
* 平台仓品审核搜索
* audit_status 默认空字符串:后端按待审过滤;选「已审核」可看历史
*/
export const formOptions: VbenFormProps = {
collapsed: false,
schema: [
{
component: 'VbenInput',
componentProps: {
placeholder: '输入药品名称',
allowClear: true,
},
fieldName: 'drug_name',
label: '药品名称',
},
{
component: 'Select',
componentProps: {
allowClear: true,
placeholder: '默认待审核',
options: [
// value 空串:后端 auditList 默认按待审过滤
{ label: '待审核', value: '' },
{ label: '已审核', value: 1 },
],
},
defaultValue: '',
fieldName: 'audit_status',
label: '审核状态',
},
],
showCollapseButton: true,
submitButtonOptions: {
content: '查询',
},
submitOnChange: true,
submitOnEnter: false,
};

View File

@@ -0,0 +1,95 @@
import type { VxeGridProps } from '#/adapter/vxe-table';
import { getDeliveryWarehouseProductAuditList } from '../api';
interface RowType {
id: number;
image?: string;
drug_name: string;
type_txt?: string;
audit_status: number;
audit_status_txt?: string;
status: number;
status_txt?: string;
status_color?: string;
uploader_name?: string;
created_at: string;
}
/**
* 平台仓品审核表格
*/
export const gridOptions: VxeGridProps<RowType> = {
checkboxConfig: {
highlight: true,
labelField: '',
},
columnConfig: {
useKey: true,
},
rowConfig: {
useKey: true,
},
columns: [
{ field: 'id', align: 'left', title: 'ID', width: 80 },
{
field: 'image',
title: '图片',
width: 80,
slots: { default: 'image' },
},
{ field: 'drug_name', title: '药品名称', minWidth: 160 },
{ field: 'type_txt', title: '类型', width: 110 },
{
field: 'audit_status_txt',
title: '审核状态',
width: 110,
slots: { default: 'audit_status' },
},
{
field: 'status_txt',
title: '商品状态',
width: 100,
slots: { default: 'status' },
},
{ field: 'uploader_name', title: '上传人', width: 120 },
{ field: 'created_at', title: '创建时间', width: 170 },
{
type: 'html',
title: '操作',
fixed: 'right',
width: 120,
slots: { default: 'action' },
},
],
keepSource: true,
pagerConfig: {},
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getDeliveryWarehouseProductAuditList({
page: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
height: 'auto',
border: false,
toolbarConfig: {
// @ts-ignore
search: true,
refresh: true,
print: false,
export: false,
zoom: true,
slots: {
buttons: 'toolbar-buttons',
},
custom: {
icon: 'vxe-icon-menu',
},
},
showOverflow: false,
};

View File

@@ -0,0 +1,127 @@
<script lang="ts" setup>
/**
* 平台「仓品审核」列表页
* 审核通过后自动打开仓药绑定弹窗,预填本仓+药品+报价
*/
import { Page, useVbenModal } from '@vben/common-ui';
import { Image, Tag } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { TableAction } from '#/components/table-action';
import BindModalDemo from '#/views/system/delivery-warehouse-drug/components/modal.vue';
import AuditModalDemo from './components/audit-modal.vue';
import { formOptions } from './config/search';
import { gridOptions } from './config/table';
defineOptions({ name: 'DeliveryWarehouseProductAudit' });
const [Grid, gridApi] = useVbenVxeGrid({
formOptions,
gridOptions,
});
const [AuditModal, auditModalApi] = useVbenModal({
connectedComponent: AuditModalDemo,
});
const [BindModal, bindModalApi] = useVbenModal({
connectedComponent: BindModalDemo,
});
/**
* 审核成功后打开仓药绑定:锁定药品、预填仓库与报价
*/
function openBindFromPreset(preset: Record<string, any>) {
if (!preset?.drug_id || !preset?.warehouse_id) {
return;
}
bindModalApi.setData({
update: false,
lockDrug: true,
presetDrug: preset.presetDrug || {
id: preset.drug_id,
drug_name: preset.drug_name,
specification: preset.specification,
image: preset.image,
},
values: {
warehouse_id: preset.warehouse_id,
drug_id: preset.drug_id,
quote: preset.quote,
status: 2,
},
gridApi,
});
bindModalApi.open();
}
/**
* 打开审核弹窗(仅待审可点)
*/
function openAudit(row: Record<string, any>) {
if (Number(row.audit_status) === 1) {
return;
}
auditModalApi.setData({
id: row.id,
values: row,
gridApi,
onBindPreset: openBindFromPreset,
});
auditModalApi.open();
}
function auditTagColor(status: number) {
return Number(status) === 1 ? 'green' : 'orange';
}
</script>
<template>
<Page auto-content-height title="仓品审核">
<AuditModal />
<BindModal />
<Grid>
<template #toolbar-buttons>
<TableAction :actions="[]" :drop-down-actions="[]" />
</template>
<template #image="{ row }">
<Image
v-if="row.image"
:src="row.image"
:height="36"
:width="36"
:preview="true"
/>
<span v-else>-</span>
</template>
<template #audit_status="{ row }">
<Tag :color="auditTagColor(row.audit_status)">
{{ row.audit_status_txt || '-' }}
</Tag>
</template>
<template #status="{ row }">
<Tag :color="row.status_color || 'default'">
{{ row.status_txt || '-' }}
</Tag>
</template>
<template #toolbar-tools></template>
<template #action="{ row }">
<TableAction
:actions="[
{
label: '审核',
type: 'link',
icon: 'mdi:clipboard-check-outline',
size: 'small',
ifShow: Number(row.audit_status) !== 1,
onClick: openAudit.bind(null, row),
},
]"
:drop-down-actions="[]"
/>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,40 @@
import { requestClient } from '#/api/request';
const prefix = 'delivery-warehouse-product/';
/**
* 仓侧「我的商品」分页列表
* 为什么单独封装:列表 / 审核列表走不同接口,避免混用参数
*/
export async function getDeliveryWarehouseProductList(data: any) {
return requestClient.get<any>(`${prefix}list`, { params: data });
}
/**
* 仓品详情(编辑回填 / 审核弹窗预览)
*/
export async function getDeliveryWarehouseProductInfo(id: number) {
return requestClient.get<any>(`${prefix}detail`, { params: { id } });
}
/**
* 仓侧新建商品(后端强制草稿 + 待审)
*/
export async function createDeliveryWarehouseProduct(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}create`, data);
}
/**
* 仓侧编辑商品
* 未审核时后端会忽略 status;已审核才可改上下架
*/
export async function updateDeliveryWarehouseProduct(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}update`, data);
}
/**
* 仓侧可选商品类型(已排除中药)
*/
export async function getDeliveryWarehouseProductTypeOptions() {
return requestClient.get<any>(`${prefix}type-options`);
}

View File

@@ -0,0 +1,139 @@
<script lang="ts" setup>
/**
* 配送仓建品弹窗
* 新建:不传 status(后端强制草稿);编辑:按 audit_status 控制 status 显隐
* 用法字典复用西药 getDrugUseList,避免各品类重复维护
*/
import { nextTick, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { getDrugUseList } from '#/views/doctor/doctor-reception/api';
import {
createDeliveryWarehouseProduct,
getDeliveryWarehouseProductInfo,
updateDeliveryWarehouseProduct,
} from '../api';
import { modalFormProps } from '../config/form';
const isUpdate = ref(false);
const gridApi = ref();
const drugTime = ref<{ label: string; value: number }[]>([]);
const drugType = ref<{ label: string; value: number }[]>([]);
const drugUnit = ref<{ label: string; value: number }[]>([]);
const drugFrequency = ref<{ label: string; value: number }[]>([]);
/** 预拉取用法字典,打开弹窗时注入 Select options */
getDrugUseList().then((res) => {
drugTime.value = (res.drug_time || []).map((item: any) => ({
label: item.name,
value: item.id,
}));
drugType.value = (res.drug_use_type || []).map((item: any) => ({
label: item.name,
value: item.id,
}));
drugUnit.value = (res.drug_unit || []).map((item: any) => ({
label: item.name,
value: item.id,
}));
drugFrequency.value = (res.drug_use_frequency || []).map((item: any) => ({
label: item.name,
value: item.id,
}));
});
const [Form, formApi] = useVbenForm(modalFormProps);
/** 把用法字典写入四个 Select,保证每次打开都有最新 options */
function injectUseDictOptions() {
formApi.updateSchema([
{ fieldName: 'time_id', componentProps: { options: drugTime.value } },
{ fieldName: 'unit_id', componentProps: { options: drugUnit.value } },
{ fieldName: 'type_id', componentProps: { options: drugType.value } },
{
fieldName: 'frequency_id',
componentProps: { options: drugFrequency.value },
},
]);
}
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();
// 新建不提交 status;未审核编辑也不提交(后端也会忽略,前端再挡一层)
const payload = { ...values };
if (!isUpdate.value || Number(payload.audit_status) === 0) {
delete payload.status;
}
delete payload.audit_status;
modalApi.setState({ loading: true, confirmLoading: true });
const submitApi = isUpdate.value
? updateDeliveryWarehouseProduct
: createDeliveryWarehouseProduct;
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) {
return;
}
injectUseDictOptions();
await formApi.resetForm();
const { values, update } = modalApi.getData<Record<string, any>>() || {};
isUpdate.value = !!update;
if (update && values?.id) {
// 编辑拉详情;zone_id 联动会清空 category_id,需 nextTick 后补回
try {
const detail = await getDeliveryWarehouseProductInfo(values.id);
const merged = {
...values,
...(detail || {}),
introduction_images: detail?.introduction_images || [],
};
const categoryId = merged.category_id;
await formApi.setValues(merged);
await nextTick();
if (categoryId) {
await formApi.setFieldValue('category_id', categoryId);
}
} catch {
await formApi.setValues(values || {});
}
} else if (values) {
await formApi.setValues(values);
}
},
});
</script>
<template>
<Modal
:title="`${isUpdate === true ? '编辑' : '新增'}仓品`"
class="w-[80%] md:w-[50%] lg:w-[40%] h-[70%]"
>
<Form />
</Modal>
</template>

View File

@@ -0,0 +1,239 @@
import type { VbenFormProps } from '#/adapter/form';
import { getSupplierOption } from '#/views/system/supplier/api';
import { getDeliveryWarehouseProductTypeOptions } from '../api';
/**
* 仓侧建品弹窗表单
* 不填药品编号/ERP倍数/分区(后端默认);必填报价供审核绑仓
*/
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'],
},
},
{
// 隐藏:驱动 status 显隐(audit_status==1 才可改上下架)
component: 'VbenInput',
fieldName: 'audit_status',
label: '审核状态',
formItemClass: 'col-span-6',
dependencies: {
show: false,
triggerFields: ['audit_status'],
},
},
{
component: 'ApiSelect',
componentProps: {
allowClear: false,
showSearch: true,
filterOption: (input: string, option: any) =>
option?.label?.toLowerCase?.().indexOf(input.toLowerCase()) >= 0,
afterFetch: (data: { label: string; value: number }[]) =>
(data || []).map((item) => ({
label: item.label,
value: item.value,
})),
api: getDeliveryWarehouseProductTypeOptions,
placeholder: '请选择商品类型',
},
fieldName: 'type',
formItemClass: 'col-span-6',
label: '商品类型',
rules: 'selectRequired',
},
{
component: 'Avatar',
fieldName: 'image',
label: '产品图片',
rules: 'required',
formItemClass: 'col-span-6',
},
{
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: getSupplierOption,
placeholder: '请选择',
},
fieldName: 'supplier_id',
formItemClass: 'col-span-6',
label: '所属供应商',
rules: 'required',
},
{
component: 'InputNumber',
componentProps: {
placeholder: '请输入报价',
min: 0.0001,
precision: 4,
class: 'w-full',
},
fieldName: 'upload_quote',
formItemClass: 'col-span-6',
label: '报价',
help: '审核通过后将按此报价绑定到本配送仓',
rules: 'required',
},
{
component: 'VbenInput',
componentProps: {
placeholder: '请输入药品昵称',
},
fieldName: 'drug_name',
formItemClass: 'col-span-6',
label: '药品昵称',
rules: 'required',
},
{
component: 'VbenInput',
componentProps: {
placeholder: '请输入药品别名',
},
fieldName: 'drug_alias',
formItemClass: 'col-span-6',
label: '药品别名',
rules: 'required',
},
{
component: 'VbenInput',
componentProps: {
placeholder: '请输入规格',
},
fieldName: 'specification',
formItemClass: 'col-span-6',
label: '规格',
rules: 'required',
},
{
component: 'VbenSelect',
componentProps: {
options: [],
placeholder: '请选择',
},
fieldName: 'time_id',
formItemClass: 'col-span-6',
label: '使用时间',
rules: 'required',
},
{
component: 'VbenSelect',
componentProps: {
options: [],
placeholder: '请选择',
},
fieldName: 'type_id',
formItemClass: 'col-span-6',
label: '使用方法',
rules: 'required',
},
{
component: 'VbenSelect',
componentProps: {
options: [],
placeholder: '请选择',
},
fieldName: 'unit_id',
formItemClass: 'col-span-6',
label: '单位',
rules: 'required',
},
{
component: 'VbenSelect',
componentProps: {
options: [],
placeholder: '请选择',
},
fieldName: 'frequency_id',
formItemClass: 'col-span-6',
label: '频率',
rules: 'required',
},
{
// 新建不展示;未审核(audit_status==0)不展示;已审核可选草稿/下架/上架
component: 'RadioGroup',
componentProps: {
options: [
{ label: '草稿', value: 1 },
{ label: '下架', value: 2 },
{ label: '上架', value: 3 },
],
},
fieldName: 'status',
formItemClass: 'col-span-12',
label: '商品状态',
dependencies: {
show(values) {
return !!values?.id && Number(values?.audit_status) === 1;
},
triggerFields: ['id', 'audit_status'],
},
},
{
component: 'RadioGroup',
componentProps: {
options: [
{ label: '是', value: 0 },
{ label: '否', value: 1 },
],
},
defaultValue: 1,
formItemClass: 'col-span-12',
fieldName: 'is_otc',
label: '是否处方药',
rules: 'required',
},
{
component: 'Textarea',
componentProps: {
placeholder: '请输入药品主治功能',
},
fieldName: 'function',
label: '药品主治功能',
rules: 'required',
},
{
component: 'Avatar',
fieldName: 'instruction',
label: '产品说明书',
rules: 'required',
formItemClass: 'col-span-6',
},
{
component: 'UploadImageSortable',
fieldName: 'introduction_images',
label: '商品介绍图',
formItemClass: 'col-span-12',
componentProps: {
maxCount: 20,
multiple: true,
},
},
],
showDefaultActions: false,
};

View File

@@ -0,0 +1,74 @@
import type { VbenFormProps } from '#/adapter/form';
import { getDeliveryWarehouseProductTypeOptions } from '../api';
/**
* 仓侧「我的商品」顶部搜索
* 字段与后端 queryField 对齐:名称模糊、类型/审核/状态精确
*/
export const formOptions: VbenFormProps = {
collapsed: false,
schema: [
{
component: 'VbenInput',
componentProps: {
placeholder: '输入药品名称',
allowClear: true,
},
fieldName: 'drug_name',
label: '药品名称',
},
{
component: 'ApiSelect',
componentProps: {
allowClear: true,
showSearch: true,
filterOption: (input: string, option: any) =>
option?.label?.toLowerCase?.().indexOf(input.toLowerCase()) >= 0,
// type-options 已是 { label, value }
afterFetch: (data: { label: string; value: number }[]) =>
(data || []).map((item) => ({
label: item.label,
value: item.value,
})),
api: getDeliveryWarehouseProductTypeOptions,
placeholder: '请选择类型',
},
fieldName: 'type',
label: '商品类型',
},
{
component: 'Select',
componentProps: {
allowClear: true,
placeholder: '请选择审核状态',
options: [
{ label: '待审核', value: 0 },
{ label: '已审核', value: 1 },
],
},
fieldName: 'audit_status',
label: '审核状态',
},
{
component: 'Select',
componentProps: {
allowClear: true,
placeholder: '请选择商品状态',
options: [
{ label: '草稿', value: 1 },
{ label: '下架', value: 2 },
{ label: '上架', value: 3 },
],
},
fieldName: 'status',
label: '商品状态',
},
],
showCollapseButton: true,
submitButtonOptions: {
content: '查询',
},
submitOnChange: true,
submitOnEnter: false,
};

View File

@@ -0,0 +1,95 @@
import type { VxeGridProps } from '#/adapter/vxe-table';
import { getDeliveryWarehouseProductList } from '../api';
interface RowType {
id: number;
image?: string;
drug_name: string;
type_txt?: string;
audit_status: number;
audit_status_txt?: string;
status: number;
status_txt?: string;
status_color?: string;
uploader_name?: string;
created_at: string;
}
/**
* 仓侧「我的商品」表格列与远程分页
*/
export const gridOptions: VxeGridProps<RowType> = {
checkboxConfig: {
highlight: true,
labelField: '',
},
columnConfig: {
useKey: true,
},
rowConfig: {
useKey: true,
},
columns: [
{ field: 'id', align: 'left', title: 'ID', width: 80 },
{
field: 'image',
title: '图片',
width: 80,
slots: { default: 'image' },
},
{ field: 'drug_name', title: '药品名称', minWidth: 160 },
{ field: 'type_txt', title: '类型', width: 110 },
{
field: 'audit_status_txt',
title: '审核状态',
width: 110,
slots: { default: 'audit_status' },
},
{
field: 'status_txt',
title: '商品状态',
width: 100,
slots: { default: 'status' },
},
{ field: 'uploader_name', title: '上传人', width: 120 },
{ field: 'created_at', title: '创建时间', width: 170 },
{
type: 'html',
title: '操作',
fixed: 'right',
width: 120,
slots: { default: 'action' },
},
],
keepSource: true,
pagerConfig: {},
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getDeliveryWarehouseProductList({
page: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
height: 'auto',
border: false,
toolbarConfig: {
// @ts-ignore
search: true,
refresh: true,
print: false,
export: false,
zoom: true,
slots: {
buttons: 'toolbar-buttons',
},
custom: {
icon: 'vxe-icon-menu',
},
},
showOverflow: false,
};

View File

@@ -0,0 +1,102 @@
<script lang="ts" setup>
/**
* 配送仓「我的商品」列表页
* 仓侧建品与上下架入口;未审核商品无专用上下架按钮(仅编辑)
*/
import { Page, useVbenModal } from '@vben/common-ui';
import { Image, Tag } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { TableAction } from '#/components/table-action';
import FormModalDemo from './components/modal.vue';
import { formOptions } from './config/search';
import { gridOptions } from './config/table';
defineOptions({ name: 'DeliveryWarehouseProduct' });
const [Grid, gridApi] = useVbenVxeGrid({
formOptions,
gridOptions,
});
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: FormModalDemo,
});
/**
* 打开新建/编辑弹窗
* @param data 行数据;新建传空对象
* @param isUpdate 是否编辑
*/
const showModal = (data: Record<string, any> = {}, isUpdate = false) => {
formModalApi.setData({
values: data,
update: isUpdate,
gridApi,
});
formModalApi.open();
};
/** 审核状态 Tag 颜色:待审橙、已审绿 */
function auditTagColor(status: number) {
return Number(status) === 1 ? 'green' : 'orange';
}
</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="[]"
/>
</template>
<template #image="{ row }">
<Image
v-if="row.image"
:src="row.image"
:height="36"
:width="36"
:preview="true"
/>
<span v-else>-</span>
</template>
<template #audit_status="{ row }">
<Tag :color="auditTagColor(row.audit_status)">
{{ row.audit_status_txt || '-' }}
</Tag>
</template>
<template #status="{ row }">
<Tag :color="row.status_color || 'default'">
{{ row.status_txt || '-' }}
</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),
},
]"
:drop-down-actions="[]"
/>
</template>
</Grid>
</Page>
</template>

View File

@@ -5,6 +5,7 @@ import { useVbenDrawer } from '@vben/common-ui';
import { Button, Descriptions, Image, Spin } from 'ant-design-vue';
import FilePreviewModal from '#/components/file-preview/FilePreviewModal.vue';
import MIcon from '#/components/icon/icon.vue';
import { XgVideoPlayer } from '#/components/video-player';
import type { FileGalleryDetail } from '#/api/core/file-gallery';
@@ -14,6 +15,8 @@ defineOptions({ name: 'FileDetailDrawer' });
const loading = ref(false);
const detail = ref<FileGalleryDetail | null>(null);
const previewOpen = ref(false);
const previewFiles = ref<string[]>([]);
const [Drawer, drawerApi] = useVbenDrawer({
class: 'w-[520px]',
@@ -45,6 +48,15 @@ function downloadFile() {
document.body.removeChild(link);
}
/** 图片/PDF 走公共预览组件 */
function openPreview() {
if (!detail.value?.url) {
return;
}
previewFiles.value = [detail.value.url];
previewOpen.value = true;
}
async function loadDetail() {
const data = drawerApi.getData<{ id?: number }>();
if (!data?.id) {
@@ -67,10 +79,11 @@ defineExpose({
<template>
<Drawer title="文件详情">
<FilePreviewModal v-model:open="previewOpen" :files="previewFiles" title="素材预览" />
<Spin :spinning="loading">
<template v-if="detail">
<div class="preview-wrap">
<Image v-if="detail.type === 0" :src="detail.url" :width="220" />
<Image v-if="detail.type === 0" :src="detail.url" :width="220" :preview="true" />
<XgVideoPlayer
v-else-if="detail.type === 1"
:src="detail.url"
@@ -84,7 +97,10 @@ defineExpose({
/>
<div v-else-if="detail.type === 5" class="fallback-preview">
<MIcon v-if="detail.type_icon" :icon="detail.type_icon" size="48" />
<Button type="primary" @click="downloadFile">下载 PDF</Button>
<div class="btn-row">
<Button type="primary" @click="openPreview">预览 PDF</Button>
<Button @click="downloadFile">下载 PDF</Button>
</div>
</div>
<div v-else class="fallback-preview">
<MIcon v-if="detail.type_icon" :icon="detail.type_icon" size="48" />
@@ -138,11 +154,22 @@ defineExpose({
align-items: center;
gap: 12px;
padding: 24px;
width: 100%;
border-radius: 8px;
border: 1px solid hsl(var(--border));
background: hsl(var(--muted) / 0.25);
color: hsl(var(--foreground));
}
.btn-row {
display: flex;
gap: 8px;
}
.type-row {
display: inline-flex;
align-items: center;
gap: 6px;
color: hsl(var(--foreground));
}
</style>

View File

@@ -357,8 +357,7 @@ watch(typesLoading, (val) => {
</template>
<style scoped lang="scss">
@use '#/components/form/components/picker-card-theme.scss' as theme;
/* 素材库卡片:用主题变量适配明/暗色,避免硬编码浅色底 */
.tab-label {
display: inline-flex;
align-items: center;
@@ -387,9 +386,10 @@ watch(typesLoading, (val) => {
flex-direction: column;
gap: 8px;
padding: 10px;
border: 1px solid #e5e6eb;
border: 1px solid hsl(var(--border));
border-radius: 8px;
background: #fff;
background: hsl(var(--card));
color: hsl(var(--card-foreground));
cursor: pointer;
}
@@ -398,7 +398,7 @@ watch(typesLoading, (val) => {
height: 120px;
object-fit: cover;
border-radius: 6px;
background: #f5f5f5;
background: hsl(var(--muted) / 0.45);
}
.gallery-file-icon {
@@ -409,8 +409,8 @@ watch(typesLoading, (val) => {
gap: 8px;
height: 120px;
border-radius: 6px;
background: #f5f5f5;
color: #4e5969;
background: hsl(var(--muted) / 0.45);
color: hsl(var(--muted-foreground));
padding: 8px;
.file-name {
@@ -425,7 +425,7 @@ watch(typesLoading, (val) => {
.gallery-meta {
font-size: 12px;
color: #86909c;
color: hsl(var(--muted-foreground));
text-align: center;
.file-title {
@@ -433,7 +433,7 @@ watch(typesLoading, (val) => {
align-items: center;
justify-content: center;
gap: 4px;
color: #1d2129;
color: hsl(var(--foreground));
font-weight: 500;
margin-bottom: 4px;
max-width: 100%;
@@ -452,23 +452,4 @@ watch(typesLoading, (val) => {
display: flex;
justify-content: center;
}
.dark {
.gallery-card {
@include theme.picker-card-dark-props;
}
.gallery-thumb,
.gallery-file-icon {
background: #374151;
}
.gallery-meta {
@include theme.picker-text-secondary-dark;
.file-title {
color: #e5e7eb;
}
}
}
</style>