1. 加工费模块重构
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CI / CI OK (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Issue Close Require / close-issues (push) Has been cancelled

2. 快递费模块重构
3. 订单导出功能优化
4. 商品订单的折扣功能
5. 修复了一些已知BUG
This commit is contained in:
李琦
2026-06-23 15:06:41 +08:00
parent 8eb479696e
commit 824dc2b0e3
34 changed files with 3852 additions and 641 deletions

1
.gitignore vendored
View File

@@ -49,3 +49,4 @@ vite.config.ts.*
*.sln
*.sw?
.history
/.cursor/

View File

@@ -44,6 +44,7 @@
"@vueuse/core": "catalog:",
"ant-design-vue": "catalog:",
"dayjs": "catalog:",
"exceljs": "^4.4.0",
"markdown-it": "^14.1.0",
"pinia": "catalog:",
"sortablejs": "catalog:",

View File

@@ -80,12 +80,83 @@ export async function expressDetailByOrderId(data: Record<string, any>) {
}
/**
* 导出订单数据
* 导出订单数据(后端 Excel旧接口保留
*/
export async function exportOrderApi() {
return requestClient.download(`${prefix}export`);
}
export interface OrderExportDataParams {
search_time: [string, string];
granularity: 'order' | 'item';
order_no?: string;
store_id?: number;
status?: number;
delivery_method?: number;
prescription_type?: number;
}
export interface OrderExportDataResult {
total: number;
rows: Record<string, unknown>[];
granularity: 'order' | 'item';
}
/**
* 导出订单 JSON 数据(供前端 ExcelJS
*/
export async function getOrderExportDataApi(params: OrderExportDataParams) {
return requestClient.get<OrderExportDataResult>(`${prefix}export-data`, { params });
}
export interface OrderExportSchemePayload {
name: string;
is_default?: boolean | number;
granularity: 'order' | 'item';
config: {
order_no?: string;
store_id?: number | null;
status?: number | null;
delivery_method?: number | null;
prescription_type?: number | null;
selected_fields: string[];
};
}
export interface OrderExportSchemeUpdatePayload {
id: number;
name?: string;
is_default?: boolean | number;
granularity?: 'order' | 'item';
config?: OrderExportSchemePayload['config'];
}
export interface OrderExportSchemeItem {
id: number;
name: string;
is_default: number;
granularity: 'order' | 'item';
config: OrderExportSchemePayload['config'];
created_at?: string;
updated_at?: string;
}
export async function getOrderExportSchemeList() {
return requestClient.get<OrderExportSchemeItem[]>(`${prefix}export-scheme-list`);
}
export async function createOrderExportScheme(data: OrderExportSchemePayload) {
return requestClient.post<OrderExportSchemeItem>(`${prefix}export-scheme-create`, data);
}
export async function updateOrderExportScheme(data: OrderExportSchemeUpdatePayload) {
return requestClient.post<OrderExportSchemeItem>(`${prefix}export-scheme-update`, data);
}
export async function deleteOrderExportScheme(data: { id: number }) {
return requestClient.post<boolean>(`${prefix}export-scheme-delete`, data);
}
/**
* 更新包邮状态
* @param data

View File

@@ -0,0 +1,151 @@
<script lang="ts" setup>
import type { Sortable } from 'sortablejs';
import { computed, nextTick, onUnmounted, ref, watch } from 'vue';
import { HolderOutlined } from '@ant-design/icons-vue';
import { Button, Modal } from 'ant-design-vue';
import type { ExportFieldMeta } from '#/views/business/order/product-order/config/exportFields';
import { buildDefaultOrderedKeys } from '#/views/business/order/product-order/utils/exportFieldOrder';
defineOptions({
name: 'ProductOrderExportFieldSortModal',
});
const props = defineProps<{
open: boolean;
orderedKeys: string[];
selectedKeys: string[];
fieldMeta: ExportFieldMeta[];
granularity: 'order' | 'item';
}>();
const emit = defineEmits<{
(e: 'update:open', value: boolean): void;
(e: 'confirm', keys: string[]): void;
}>();
const listRef = ref<HTMLElement | null>(null);
const draftKeys = ref<string[]>([]);
let sortable: Sortable | null = null;
const labelMap = computed(
() => new Map(props.fieldMeta.map((f) => [f.key, f.label])),
);
const draftItems = computed(() =>
draftKeys.value.map((key) => ({
key,
label: labelMap.value.get(key) ?? key,
})),
);
function destroySortable() {
sortable?.destroy();
sortable = null;
}
async function initSortable() {
destroySortable();
await nextTick();
if (!listRef.value || !props.open) {
return;
}
const mod = await import(
// @ts-expect-error sortablejs modular esm path
'sortablejs/modular/sortable.complete.esm.js'
);
sortable = mod.default.create(listRef.value, {
animation: 200,
handle: '.drag-handle',
ghostClass: 'export-field-sort-item--ghost',
onEnd() {
if (!listRef.value) {
return;
}
const keys: string[] = [];
listRef.value.querySelectorAll('[data-key]').forEach((el) => {
const key = el.getAttribute('data-key');
if (key) {
keys.push(key);
}
});
draftKeys.value = keys;
},
});
}
watch(
() => props.open,
(open) => {
if (open) {
draftKeys.value = [...props.orderedKeys];
void initSortable();
} else {
destroySortable();
}
},
);
onUnmounted(() => {
destroySortable();
});
function handleCancel() {
emit('update:open', false);
}
function handleConfirm() {
emit('confirm', [...draftKeys.value]);
emit('update:open', false);
}
function handleReset() {
draftKeys.value = buildDefaultOrderedKeys(
props.selectedKeys,
props.granularity,
);
void initSortable();
}
</script>
<template>
<Modal
:open="open"
title="字段排序"
width="480px"
ok-text="确认"
cancel-text="取消"
@cancel="handleCancel"
@ok="handleConfirm"
>
<div class="mb-3 text-xs text-muted-foreground">
拖拽左侧手柄调整 Excel 导出列顺序金额类字段默认会聚合在一起
</div>
<div
ref="listRef"
class="max-h-[360px] overflow-y-auto rounded-md border border-border"
>
<div
v-for="item in draftItems"
:key="item.key"
:data-key="item.key"
class="export-field-sort-item flex items-center gap-2 border-b border-border px-3 py-2 last:border-b-0"
>
<HolderOutlined class="drag-handle cursor-grab text-muted-foreground" />
<span class="text-sm">{{ item.label }}</span>
</div>
</div>
<div class="mt-3">
<Button size="small" @click="handleReset">重置为默认顺序</Button>
</div>
</Modal>
</template>
<style scoped>
.export-field-sort-item--ghost {
opacity: 0.5;
background: hsl(var(--muted));
}
</style>

View File

@@ -0,0 +1,851 @@
<script lang="ts" setup>
import type { Dayjs } from 'dayjs';
import { computed, ref, watch } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import dayjs from 'dayjs';
import {
Button,
Checkbox,
DatePicker,
Divider,
Input,
Modal as AntModal,
message,
Radio,
Select,
Space,
Tag,
} from 'ant-design-vue';
import { downloadByData } from '#/util/tool';
import {
createOrderExportScheme,
deleteOrderExportScheme,
getOrderExportDataApi,
getOrderExportSchemeList,
getOrderStatusOption,
type OrderExportDataParams,
type OrderExportSchemeItem,
updateOrderExportScheme,
} from '#/views/business/order/product-order/api';
import ProductOrderExportFieldSortModal from '#/views/business/order/product-order/components/ProductOrderExportFieldSortModal.vue';
import type { ExportGranularity } from '#/views/business/order/product-order/config/exportFields';
import {
getDefaultSelectedKeys,
getExportFields,
granularityLabel,
groupExportFields,
} from '#/views/business/order/product-order/config/exportFields';
import {
buildDefaultOrderedKeys,
formatFieldOrderPreview,
syncOrderedSelectedKeys,
} from '#/views/business/order/product-order/utils/exportFieldOrder';
import {
exportFilename,
exportProductOrderExcel,
} from '#/views/business/order/product-order/utils/exportProductOrderExcel';
import {
applySchemeToForm,
buildSchemeConfigFromForm,
findDefaultScheme,
type ExportFormState,
} from '#/views/business/order/product-order/utils/exportScheme';
import { getStoreOption } from '#/views/system/store/api';
defineOptions({
name: 'ProductOrderExportModal',
});
const CUSTOM_SCHEME_VALUE = 0;
const DELIVERY_OPTIONS = [
{ label: '快递到家', value: 0 },
{ label: '诊所自提', value: 1 },
];
const PRESCRIPTION_TYPE_OPTIONS = [
{ label: '中药', value: 1 },
{ label: '西药', value: 2 },
{ label: '中成药', value: 3 },
{ label: '产品服务包', value: 5 },
];
const searchTime = ref<[Dayjs, Dayjs]>([dayjs().startOf('month'), dayjs()]);
const orderNo = ref('');
const storeId = ref<number | undefined>(undefined);
const status = ref<number | undefined>(undefined);
const deliveryMethod = ref<number | undefined>(undefined);
const prescriptionType = ref<number | undefined>(undefined);
const granularity = ref<ExportGranularity>('order');
const selectedKeys = ref<string[]>(getDefaultSelectedKeys('order'));
const orderedSelectedKeys = ref<string[]>(
buildDefaultOrderedKeys(getDefaultSelectedKeys('order'), 'order'),
);
const fieldOrderCustomized = ref(false);
const loading = ref(false);
const loadingStep = ref('');
const storeOptions = ref<{ label: string; value: number }[]>([]);
const statusOptions = ref<{ label: string; value: number }[]>([]);
const fetchingStores = ref(false);
const storeExportLocked = ref(false);
const lockedStoreName = ref('');
const schemes = ref<OrderExportSchemeItem[]>([]);
const activeSchemeId = ref<number>(CUSTOM_SCHEME_VALUE);
const applyingScheme = ref(false);
const schemesLoading = ref(false);
const saveModalOpen = ref(false);
const saveModalLoading = ref(false);
const saveSchemeName = ref('');
const saveSchemeDefault = ref(false);
const sortModalOpen = ref(false);
const updateSchemeLoading = ref(false);
const defaultTagLoading = ref(false);
const schemeModified = ref(false);
const fieldList = computed(() => getExportFields(granularity.value));
const groupedFields = computed(() => groupExportFields(fieldList.value));
const selectedCount = computed(() => selectedKeys.value.length);
const totalFieldCount = computed(() => fieldList.value.length);
const fieldOrderPreview = computed(() =>
orderedSelectedKeys.value.length > 0
? formatFieldOrderPreview(orderedSelectedKeys.value, fieldList.value)
: '',
);
const schemeSelectOptions = computed(() => [
{ label: '自定义', value: CUSTOM_SCHEME_VALUE },
...schemes.value.map((s) => {
let label = s.name;
if (schemeModified.value && s.id === activeSchemeId.value) {
label += '(已修改)';
}
return { label, value: s.id };
}),
]);
const activeSavedScheme = computed(() =>
schemes.value.find((s) => s.id === activeSchemeId.value),
);
const formState = (): ExportFormState => ({
orderNo: orderNo.value,
storeId: storeId.value,
status: status.value,
deliveryMethod: deliveryMethod.value,
prescriptionType: prescriptionType.value,
granularity: granularity.value,
selectedKeys: selectedKeys.value,
orderedSelectedKeys: orderedSelectedKeys.value,
});
function markSchemeModified() {
if (activeSchemeId.value > CUSTOM_SCHEME_VALUE) {
schemeModified.value = true;
}
}
function clearSchemeModified() {
schemeModified.value = false;
}
function syncOrderFromSelection() {
orderedSelectedKeys.value = syncOrderedSelectedKeys(
selectedKeys.value,
orderedSelectedKeys.value,
granularity.value,
fieldOrderCustomized.value,
);
}
function searchTimeRange(): [string, string] {
return [
searchTime.value[0].format('YYYY-MM-DD'),
searchTime.value[1].format('YYYY-MM-DD'),
];
}
function labelOf(
options: { label: string; value: number }[],
value: number | undefined,
): string {
if (value === undefined || value === null) {
return '全部';
}
return options.find((o) => o.value === value)?.label ?? String(value);
}
function storeNameOf(id: number | undefined): string {
if (id == null) {
return '全部';
}
if (storeExportLocked.value && lockedStoreName.value) {
return lockedStoreName.value;
}
return storeOptions.value.find((o) => o.value === id)?.label ?? String(id);
}
function selectedFieldLabels(): string {
const labelMap = new Map(fieldList.value.map((f) => [f.key, f.label]));
return orderedSelectedKeys.value
.map((k) => labelMap.get(k) ?? k)
.join('、');
}
function resetFormDefaults() {
orderNo.value = '';
status.value = undefined;
deliveryMethod.value = undefined;
prescriptionType.value = undefined;
granularity.value = 'order';
selectedKeys.value = getDefaultSelectedKeys('order');
orderedSelectedKeys.value = buildDefaultOrderedKeys(
selectedKeys.value,
'order',
);
fieldOrderCustomized.value = false;
if (!storeExportLocked.value) {
storeId.value = undefined;
}
}
function applyScheme(scheme: OrderExportSchemeItem) {
applyingScheme.value = true;
try {
const state = formState();
applySchemeToForm(scheme, state);
granularity.value = state.granularity;
orderNo.value = state.orderNo;
if (!storeExportLocked.value) {
storeId.value = state.storeId;
}
status.value = state.status;
deliveryMethod.value = state.deliveryMethod;
prescriptionType.value = state.prescriptionType;
selectedKeys.value = state.selectedKeys;
orderedSelectedKeys.value = state.orderedSelectedKeys;
fieldOrderCustomized.value = true;
clearSchemeModified();
} finally {
applyingScheme.value = false;
}
}
async function loadSchemes(applyDefault = false) {
schemesLoading.value = true;
try {
const list = await getOrderExportSchemeList();
schemes.value = list ?? [];
if (applyDefault) {
const def = findDefaultScheme(schemes.value);
if (def) {
activeSchemeId.value = def.id;
applyScheme(def);
} else {
activeSchemeId.value = CUSTOM_SCHEME_VALUE;
resetFormDefaults();
}
}
} catch {
schemes.value = [];
} finally {
schemesLoading.value = false;
}
}
function onSchemeSelectChange(value: number) {
activeSchemeId.value = value;
if (value === CUSTOM_SCHEME_VALUE) {
clearSchemeModified();
resetFormDefaults();
return;
}
const scheme = schemes.value.find((s) => s.id === value);
if (scheme) {
applyScheme(scheme);
}
}
watch(granularity, (g) => {
if (applyingScheme.value) {
return;
}
selectedKeys.value = getDefaultSelectedKeys(g);
orderedSelectedKeys.value = buildDefaultOrderedKeys(selectedKeys.value, g);
fieldOrderCustomized.value = false;
markSchemeModified();
});
watch(
[orderNo, storeId, status, deliveryMethod, prescriptionType],
() => {
if (applyingScheme.value) {
return;
}
markSchemeModified();
},
);
watch(
selectedKeys,
() => {
if (applyingScheme.value) {
return;
}
syncOrderFromSelection();
},
{ deep: true },
);
const loadStores = () => {
fetchingStores.value = true;
getStoreOption({})
.then((res: { id: number; name: string }[]) => {
storeOptions.value = (res || []).map((item) => ({
label: `${item.name}${item.id}`,
value: item.id,
}));
})
.finally(() => {
fetchingStores.value = false;
});
};
const loadStatusOptions = () => {
getOrderStatusOption({}).then((res: { id: number; name: string }[]) => {
statusOptions.value = (res || []).map((item) => ({
label: item.name,
value: item.id,
}));
});
};
function onSelectedKeysChange() {
markSchemeModified();
}
function selectAllFields() {
selectedKeys.value = fieldList.value.map((f) => f.key);
fieldOrderCustomized.value = false;
markSchemeModified();
}
function clearAllFields() {
selectedKeys.value = [];
orderedSelectedKeys.value = [];
fieldOrderCustomized.value = false;
markSchemeModified();
}
function resetDefaultFields() {
selectedKeys.value = getDefaultSelectedKeys(granularity.value);
orderedSelectedKeys.value = buildDefaultOrderedKeys(
selectedKeys.value,
granularity.value,
);
fieldOrderCustomized.value = false;
markSchemeModified();
}
function toggleGroup(group: string, checked: boolean) {
const keys = groupedFields.value[group]?.map((f) => f.key) ?? [];
if (checked) {
selectedKeys.value = [...new Set([...selectedKeys.value, ...keys])];
} else {
selectedKeys.value = selectedKeys.value.filter((k) => !keys.includes(k));
}
markSchemeModified();
}
function isGroupAllChecked(group: string): boolean {
const keys = groupedFields.value[group]?.map((f) => f.key) ?? [];
return keys.length > 0 && keys.every((k) => selectedKeys.value.includes(k));
}
function popupContainerBody() {
return document.body;
}
function openFieldSortModal() {
if (orderedSelectedKeys.value.length === 0) {
message.warning('请至少选择一个导出字段');
return;
}
sortModalOpen.value = true;
}
function onFieldSortConfirm(keys: string[]) {
orderedSelectedKeys.value = keys;
const defaultOrder = buildDefaultOrderedKeys(
selectedKeys.value,
granularity.value,
);
fieldOrderCustomized.value = keys.join('\0') !== defaultOrder.join('\0');
markSchemeModified();
}
function openSaveSchemeModal() {
if (orderedSelectedKeys.value.length === 0) {
message.warning('请至少选择一个导出字段');
return;
}
saveSchemeName.value = '';
saveSchemeDefault.value = false;
saveModalOpen.value = true;
}
async function confirmSaveScheme() {
const name = saveSchemeName.value.trim();
if (!name) {
message.warning('请输入方案名称');
return;
}
saveModalLoading.value = true;
try {
const saved = await createOrderExportScheme({
name,
is_default: saveSchemeDefault.value ? 1 : 0,
granularity: granularity.value,
config: buildSchemeConfigFromForm(formState()),
});
message.success('方案已保存');
await loadSchemes(false);
activeSchemeId.value = saved.id;
fieldOrderCustomized.value = true;
clearSchemeModified();
saveModalOpen.value = false;
} catch (e: any) {
message.error(e?.message || '保存失败');
} finally {
saveModalLoading.value = false;
}
}
async function handleUpdateScheme() {
if (activeSchemeId.value <= CUSTOM_SCHEME_VALUE || !activeSavedScheme.value) {
return;
}
if (orderedSelectedKeys.value.length === 0) {
message.warning('请至少选择一个导出字段');
return;
}
const scheme = activeSavedScheme.value;
AntModal.confirm({
title: '更新导出方案',
content: `将当前配置同步至方案「${scheme.name}」,是否继续?`,
onOk: async () => {
updateSchemeLoading.value = true;
try {
await updateOrderExportScheme({
id: scheme.id,
granularity: granularity.value,
config: buildSchemeConfigFromForm(formState()),
});
message.success('方案已更新');
await loadSchemes(false);
activeSchemeId.value = scheme.id;
clearSchemeModified();
} catch (e: any) {
message.error(e?.message || '更新失败');
} finally {
updateSchemeLoading.value = false;
}
},
});
}
async function toggleSchemeDefault() {
const scheme = activeSavedScheme.value;
if (!scheme || defaultTagLoading.value) {
return;
}
const nextDefault = scheme.is_default === 1 ? 0 : 1;
defaultTagLoading.value = true;
try {
await updateOrderExportScheme({
id: scheme.id,
is_default: nextDefault,
});
message.success(nextDefault === 1 ? '已设为默认方案' : '已取消默认方案');
await loadSchemes(false);
activeSchemeId.value = scheme.id;
} catch (e: any) {
message.error(e?.message || '操作失败');
} finally {
defaultTagLoading.value = false;
}
}
async function handleDeleteScheme() {
if (activeSchemeId.value <= CUSTOM_SCHEME_VALUE) {
return;
}
AntModal.confirm({
title: '删除导出方案',
content: `确定删除方案「${activeSavedScheme.value?.name ?? ''}」吗?`,
okType: 'danger',
onOk: async () => {
await deleteOrderExportScheme({ id: activeSchemeId.value });
message.success('已删除');
activeSchemeId.value = CUSTOM_SCHEME_VALUE;
clearSchemeModified();
resetFormDefaults();
await loadSchemes(true);
},
});
}
const [Modal, modalApi] = useVbenModal({
fullscreen: true,
fullscreenButton: true,
draggable: true,
onCancel() {
modalApi.close();
},
onConfirm: async () => {
if (!searchTime.value?.[0] || !searchTime.value?.[1]) {
message.warning('请选择时间范围');
return;
}
if (orderedSelectedKeys.value.length === 0) {
message.warning('请至少选择一个导出字段');
return;
}
loading.value = true;
loadingStep.value = '正在拉取数据…';
try {
const timeRange = searchTimeRange();
const params: OrderExportDataParams = {
search_time: timeRange,
granularity: granularity.value,
};
if (orderNo.value.trim()) {
params.order_no = orderNo.value.trim();
}
if (storeId.value != null) {
params.store_id = storeId.value;
}
if (status.value != null) {
params.status = status.value;
}
if (deliveryMethod.value != null) {
params.delivery_method = deliveryMethod.value;
}
if (prescriptionType.value != null) {
params.prescription_type = prescriptionType.value;
}
const res = await getOrderExportDataApi(params);
const rows = res?.rows ?? [];
if (rows.length === 0) {
message.warning('暂无符合条件的数据');
return;
}
loadingStep.value = '正在生成 Excel…';
const fieldsMeta = getExportFields(granularity.value);
const buffer = await exportProductOrderExcel({
rows,
selectedFields: orderedSelectedKeys.value,
fieldMeta: fieldsMeta,
granularity: granularity.value,
exportedAt: new Date(),
filters: {
search_time: timeRange,
order_no: orderNo.value.trim() || undefined,
store_name: storeNameOf(storeId.value),
status_text: labelOf(statusOptions.value, status.value),
delivery_method_text: labelOf(DELIVERY_OPTIONS, deliveryMethod.value),
prescription_type_text: labelOf(
PRESCRIPTION_TYPE_OPTIONS,
prescriptionType.value,
),
export_fields_text: selectedFieldLabels(),
row_count: rows.length,
},
});
downloadByData(
buffer,
exportFilename(granularity.value, timeRange),
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
);
message.success(`导出成功,共 ${rows.length}`);
modalApi.close();
} catch (e: any) {
message.error(e?.message || '导出失败');
} finally {
loading.value = false;
loadingStep.value = '';
}
},
onOpenChange(isOpen: boolean) {
if (isOpen) {
modalApi.setState({ fullscreen: true });
const data =
modalApi.getData<{
store_export_locked?: boolean;
locked_store_id?: number;
locked_store_name?: string;
}>() || {};
searchTime.value = [dayjs().startOf('month'), dayjs()];
activeSchemeId.value = CUSTOM_SCHEME_VALUE;
clearSchemeModified();
resetFormDefaults();
storeExportLocked.value = !!data.store_export_locked;
if (storeExportLocked.value && data.locked_store_id != null) {
storeId.value = data.locked_store_id;
lockedStoreName.value = data.locked_store_name || '本诊所';
} else {
storeId.value = undefined;
lockedStoreName.value = '';
loadStores();
}
loadStatusOptions();
void loadSchemes(true);
}
},
});
</script>
<template>
<Modal
:confirm-loading="loading"
:ok-text="loading ? loadingStep || '导出中' : '导出 Excel'"
title="导出商品订单"
>
<div class="flex max-h-[calc(100vh-220px)] flex-col gap-4 py-1">
<!-- 导出方案 -->
<div>
<div class="mb-2 text-sm font-medium">导出方案</div>
<div
class="flex flex-wrap items-center gap-2 rounded-md border border-border bg-muted/20 p-3"
>
<Select
:value="activeSchemeId"
class="min-w-[200px] flex-1"
:loading="schemesLoading"
:options="schemeSelectOptions"
placeholder="选择导出方案"
@change="onSchemeSelectChange"
/>
<Tag
v-if="activeSchemeId > CUSTOM_SCHEME_VALUE && activeSavedScheme"
class="cursor-pointer select-none"
:color="activeSavedScheme.is_default === 1 ? 'gold' : undefined"
@click.stop="toggleSchemeDefault"
>
{{ defaultTagLoading ? '…' : activeSavedScheme.is_default === 1 ? '默认' : '设为默认' }}
</Tag>
<Button
v-if="activeSchemeId > CUSTOM_SCHEME_VALUE"
:loading="updateSchemeLoading"
type="primary"
@click="handleUpdateScheme"
>
更新方案
</Button>
<Button type="primary" ghost @click="openSaveSchemeModal">保存方案</Button>
<Button
v-if="activeSchemeId > CUSTOM_SCHEME_VALUE"
danger
@click="handleDeleteScheme"
>
删除方案
</Button>
</div>
<div
v-if="schemeModified && activeSchemeId > CUSTOM_SCHEME_VALUE"
class="mt-2 text-xs text-amber-600 dark:text-amber-500"
>
当前方案已修改点击更新方案同步保存
</div>
</div>
<!-- 区块 A筛选条件 -->
<div>
<div class="mb-2 text-sm font-medium">筛选条件</div>
<div
class="grid grid-cols-2 gap-x-4 gap-y-3 rounded-md border border-border bg-muted/20 p-3"
>
<div class="col-span-2">
<div class="mb-1 text-xs text-muted-foreground">时间范围 *</div>
<DatePicker.RangePicker
v-model:value="searchTime"
class="w-full"
format="YYYY-MM-DD"
:get-popup-container="popupContainerBody"
/>
</div>
<div>
<div class="mb-1 text-xs text-muted-foreground">订单号</div>
<Input v-model:value="orderNo" allow-clear placeholder="输入订单号" />
</div>
<div>
<div class="mb-1 text-xs text-muted-foreground">门店</div>
<div
v-if="storeExportLocked"
class="rounded border bg-background px-3 py-1.5 text-sm"
>
{{ lockedStoreName || '本诊所' }}ID: {{ storeId }}
</div>
<Select
v-else
v-model:value="storeId"
allow-clear
show-search
class="w-full"
:filter-option="
(input: string, option: any) =>
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
"
:loading="fetchingStores"
:options="storeOptions"
placeholder="全部"
option-filter-prop="label"
/>
</div>
<div>
<div class="mb-1 text-xs text-muted-foreground">订单状态</div>
<Select
v-model:value="status"
allow-clear
class="w-full"
:options="statusOptions"
placeholder="全部"
/>
</div>
<div>
<div class="mb-1 text-xs text-muted-foreground">发货方式</div>
<Select
v-model:value="deliveryMethod"
allow-clear
class="w-full"
:options="DELIVERY_OPTIONS"
placeholder="全部"
/>
</div>
<div>
<div class="mb-1 text-xs text-muted-foreground">订单类型</div>
<Select
v-model:value="prescriptionType"
allow-clear
class="w-full"
:options="PRESCRIPTION_TYPE_OPTIONS"
placeholder="全部"
/>
</div>
</div>
</div>
<!-- 区块 B导出设置 -->
<div>
<div class="mb-2 text-sm font-medium">导出设置</div>
<Radio.Group v-model:value="granularity">
<Radio value="order">订单维度(一行一个订单)</Radio>
<Radio value="item">商品明细维度(一行一个商品)</Radio>
</Radio.Group>
<div class="mt-1 text-xs text-muted-foreground">
当前粒度:{{ granularityLabel(granularity) }},导出文件将包含筛选条件说明与专业表头样式
</div>
</div>
<Divider class="my-0" />
<!-- 区块 C导出字段 -->
<div class="flex min-h-0 flex-1 flex-col">
<div class="mb-2 flex items-center justify-between">
<span class="text-sm font-medium">导出字段</span>
<Space size="small">
<span class="text-xs text-muted-foreground">
已选 {{ selectedCount }} / {{ totalFieldCount }}
</span>
<Button
size="small"
type="link"
:disabled="orderedSelectedKeys.length === 0"
@click="openFieldSortModal"
>
字段排序
</Button>
<Button size="small" type="link" @click="selectAllFields">全选</Button>
<Button size="small" type="link" @click="clearAllFields">取消全选</Button>
<Button size="small" type="link" @click="resetDefaultFields">恢复默认</Button>
</Space>
</div>
<Checkbox.Group
v-model:value="selectedKeys"
class="min-h-0 flex-1 overflow-y-auto rounded-md border border-border p-3"
@change="onSelectedKeysChange"
>
<div
v-for="(fields, group) in groupedFields"
:key="group"
class="mb-3 last:mb-0"
>
<div class="mb-1.5 flex items-center gap-2">
<Checkbox
:checked="isGroupAllChecked(group)"
@change="(e: any) => toggleGroup(group, e.target.checked)"
>
<span class="text-xs font-medium text-muted-foreground">{{ group }}</span>
</Checkbox>
</div>
<div class="grid grid-cols-3 gap-x-2 gap-y-1">
<Checkbox v-for="field in fields" :key="field.key" :value="field.key">
<span class="text-sm">{{ field.label }}</span>
</Checkbox>
</div>
</div>
</Checkbox.Group>
<div
v-if="fieldOrderPreview"
class="mt-2 rounded-md border border-dashed border-border bg-muted/10 px-3 py-2 text-xs leading-relaxed text-muted-foreground"
>
<span class="font-medium text-foreground">导出列顺序:</span>
{{ fieldOrderPreview }}
</div>
</div>
</div>
<ProductOrderExportFieldSortModal
v-model:open="sortModalOpen"
:field-meta="fieldList"
:granularity="granularity"
:ordered-keys="orderedSelectedKeys"
:selected-keys="selectedKeys"
@confirm="onFieldSortConfirm"
/>
<AntModal
v-model:open="saveModalOpen"
:confirm-loading="saveModalLoading"
title="保存导出方案"
ok-text="保存"
@ok="confirmSaveScheme"
>
<div class="flex flex-col gap-3 py-2">
<div>
<div class="mb-1 text-sm text-muted-foreground">方案名称</div>
<Input v-model:value="saveSchemeName" allow-clear placeholder="请输入方案名称" />
</div>
<Checkbox v-model:checked="saveSchemeDefault">设为默认方案打开弹窗时自动套用</Checkbox>
</div>
</AntModal>
</Modal>
</template>

View File

@@ -0,0 +1,98 @@
export type ExportFieldFormat = 'text' | 'currency' | 'datetime' | 'integer';
export type ExportFieldAlign = 'left' | 'center' | 'right';
export type ExportGranularity = 'order' | 'item';
export interface ExportFieldMeta {
key: string;
label: string;
default: boolean;
width: number;
align: ExportFieldAlign;
group: string;
format: ExportFieldFormat;
/** 汇总行是否对该列求和 */
summary?: boolean;
}
export const ORDER_EXPORT_FIELDS: ExportFieldMeta[] = [
{ key: 'id', label: 'ID', default: false, width: 10, align: 'center', group: '基础信息', format: 'integer' },
{ key: 'order_no', label: '订单号', default: true, width: 22, align: 'left', group: '基础信息', format: 'text' },
{ key: 'store_name', label: '诊所名称', default: true, width: 18, align: 'left', group: '基础信息', format: 'text' },
{ key: 'warehouse_name', label: '仓库', default: false, width: 14, align: 'left', group: '基础信息', format: 'text' },
{ key: 'doctor_name', label: '医生', default: true, width: 12, align: 'left', group: '基础信息', format: 'text' },
{ key: 'patient_name', label: '就诊人', default: false, width: 12, align: 'left', group: '基础信息', format: 'text' },
{ key: 'order_type_text', label: '订单类型', default: true, width: 14, align: 'center', group: '基础信息', format: 'text' },
{ key: 'prescription_type_text', label: '处方类型', default: true, width: 14, align: 'center', group: '基础信息', format: 'text' },
{ key: 'pay_type_text', label: '支付类型', default: false, width: 12, align: 'center', group: '基础信息', format: 'text' },
{ key: 'dosage', label: '中药剂数', default: false, width: 10, align: 'center', group: '基础信息', format: 'integer' },
{ key: 'is_free_shipping_text', label: '是否包邮', default: false, width: 10, align: 'center', group: '基础信息', format: 'text' },
{ key: 'items_price', label: '商品总价', default: true, width: 12, align: 'right', group: '金额', format: 'currency', summary: true },
{ key: 'market_price', label: '采购价', default: false, width: 12, align: 'right', group: '金额', format: 'currency', summary: true },
{ key: 'decoct_price', label: '代煎费用', default: false, width: 12, align: 'right', group: '金额', format: 'currency', summary: true },
{ key: 'process_price', label: '加工费用', default: false, width: 12, align: 'right', group: '金额', format: 'currency', summary: true },
{ key: 'treatement_price', label: '诊疗费用', default: false, width: 12, align: 'right', group: '金额', format: 'currency', summary: true },
{ key: 'total_pay_price', label: '支付总价', default: true, width: 12, align: 'right', group: '金额', format: 'currency', summary: true },
{ key: 'trans_expenses', label: '运费', default: false, width: 10, align: 'right', group: '金额', format: 'currency', summary: true },
{ key: 'pay_time', label: '支付时间', default: true, width: 20, align: 'center', group: '时间', format: 'datetime' },
{ key: 'status_text', label: '订单状态', default: true, width: 12, align: 'center', group: '状态', format: 'text' },
{ key: 'pay_method_text', label: '订单来源', default: false, width: 16, align: 'center', group: '状态', format: 'text' },
{ key: 'delivery_method_text', label: '配送方式', default: true, width: 10, align: 'center', group: '状态', format: 'text' },
{ key: 'remark', label: '留言备注', default: false, width: 20, align: 'left', group: '收货信息', format: 'text' },
{ key: 'express_name', label: '收货人', default: true, width: 12, align: 'left', group: '收货信息', format: 'text' },
{ key: 'express_mobile', label: '收货电话', default: true, width: 14, align: 'left', group: '收货信息', format: 'text' },
{ key: 'express_region', label: '收货省市区', default: false, width: 18, align: 'left', group: '收货信息', format: 'text' },
{ key: 'express_address', label: '收货地址', default: true, width: 24, align: 'left', group: '收货信息', format: 'text' },
{ key: 'is_send_text', label: '是否发货', default: false, width: 10, align: 'center', group: '发货', format: 'text' },
{ key: 'send_time', label: '发货时间', default: false, width: 20, align: 'center', group: '发货', format: 'datetime' },
{ key: 'introduce', label: '发货备注', default: false, width: 20, align: 'left', group: '发货', format: 'text' },
{ key: 'created_at', label: '创建时间', default: true, width: 20, align: 'center', group: '时间', format: 'datetime' },
];
export const ITEM_EXPORT_FIELDS: ExportFieldMeta[] = [
{ key: 'epl_order_no', label: '易票联订单号', default: false, width: 22, align: 'left', group: '基础信息', format: 'text' },
{ key: 'order_no', label: '订单号', default: true, width: 22, align: 'left', group: '基础信息', format: 'text' },
{ key: 'store_name', label: '诊所名称', default: true, width: 18, align: 'left', group: '基础信息', format: 'text' },
{ key: 'drug_name', label: '商品名称', default: true, width: 20, align: 'left', group: '商品', format: 'text' },
{ key: 'specification', label: '规格', default: true, width: 14, align: 'left', group: '商品', format: 'text' },
{ key: 'manufacturer', label: '厂家', default: true, width: 16, align: 'left', group: '商品', format: 'text' },
{ key: 'unit_price', label: '单价', default: true, width: 12, align: 'right', group: '金额', format: 'currency' },
{ key: 'number', label: '数量', default: true, width: 10, align: 'center', group: '商品', format: 'integer' },
{ key: 'line_amount', label: '行金额', default: true, width: 12, align: 'right', group: '金额', format: 'currency', summary: true },
{ key: 'process_price', label: '加工费', default: false, width: 12, align: 'right', group: '金额', format: 'currency' },
{ key: 'treatement_price', label: '诊疗费', default: false, width: 12, align: 'right', group: '金额', format: 'currency' },
{ key: 'total_pay_price', label: '订单总价', default: true, width: 12, align: 'right', group: '金额', format: 'currency' },
{ key: 'trans_expenses', label: '运费', default: false, width: 10, align: 'right', group: '金额', format: 'currency' },
{ key: 'doctor_name', label: '医生', default: true, width: 12, align: 'left', group: '基础信息', format: 'text' },
{ key: 'status_text', label: '订单状态', default: true, width: 12, align: 'center', group: '状态', format: 'text' },
{ key: 'refund_status_text', label: '售后状态', default: false, width: 12, align: 'center', group: '状态', format: 'text' },
{ key: 'created_at', label: '下单时间', default: true, width: 20, align: 'center', group: '时间', format: 'datetime' },
{ key: 'express_name', label: '收件人', default: true, width: 12, align: 'left', group: '收货信息', format: 'text' },
{ key: 'express_mobile', label: '电话', default: true, width: 14, align: 'left', group: '收货信息', format: 'text' },
{ key: 'express_address', label: '地址', default: true, width: 28, align: 'left', group: '收货信息', format: 'text' },
{ key: 'pay_time', label: '支付时间', default: true, width: 20, align: 'center', group: '时间', format: 'datetime' },
];
export function getExportFields(granularity: ExportGranularity): ExportFieldMeta[] {
return granularity === 'item' ? ITEM_EXPORT_FIELDS : ORDER_EXPORT_FIELDS;
}
export function getDefaultSelectedKeys(granularity: ExportGranularity): string[] {
return getExportFields(granularity)
.filter((f) => f.default)
.map((f) => f.key);
}
export function groupExportFields(fields: ExportFieldMeta[]): Record<string, ExportFieldMeta[]> {
return fields.reduce<Record<string, ExportFieldMeta[]>>((acc, field) => {
const group = field.group;
if (!acc[group]) {
acc[group] = [];
}
acc[group]!.push(field);
return acc;
}, {});
}
export function granularityLabel(granularity: ExportGranularity): string {
return granularity === 'item' ? '商品明细维度' : '订单维度';
}

View File

@@ -17,9 +17,7 @@ import { Button, Image, message, Modal as AntdModal, Popover, Space, Switch, Tab
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { TableAction } from '#/components/table-action';
import { downloadByData } from '#/util/tool';
import {
exportOrderApi,
getOrderInfo,
getOrderList,
getVerifyRecentOrderAmounts,
@@ -37,6 +35,7 @@ import PrescriptionDetail from '#/views/doctor/doctor-reception/components/Presc
import DetailModal from './components/detail.vue';
import FormModalDemo from './components/modal.vue';
import ProductOrderExportModal from './components/ProductOrderExportModal.vue';
import Refund from './components/refund.vue';
import { formOptions } from './config/search';
import { gridOptions } from './config/table';
@@ -130,6 +129,10 @@ const [RefundModal, RefundModalApi] = useVbenModal({
connectedComponent: Refund,
});
const [ExportModal, exportModalApi] = useVbenModal({
connectedComponent: ProductOrderExportModal,
});
const infoModal = (data = {}) => {
modalApi.setData({
// 表单值
@@ -327,15 +330,10 @@ const openPrescriptionDetail = (values) => {
};
/**
* 西药导出
* 打开导出弹窗
*/
const passApplication = () => {
// 新标签跳转到 exportWesternMedicineApi
exportOrderApi().then((res) => {
// 创建新的URL表示指定的File对象或者Blob对象。
downloadByData(res.data, `萧康云医-商品订单导出${new Date()}.xlsx`);
message.success('导出成功!');
});
const openExportModal = () => {
exportModalApi.open();
};
const openRefundModal = (id) => {
@@ -457,6 +455,7 @@ const openOrderAmountVerify = () => {
</Table>
</AntdModal>
<RefundModal />
<ExportModal />
<PrescriptionDetailModal />
<TraceDrawer />
<ChinaErpSyncDrawer @synced="() => gridApi.query()" />
@@ -477,7 +476,7 @@ const openOrderAmountVerify = () => {
type: 'primary',
icon: 'ix:export-check',
// auth: ['超级西(中成)药', 'sys:user:save'],
onClick: passApplication.bind(null),
onClick: openExportModal.bind(null),
},
{
label: '展开全部',

View File

@@ -0,0 +1,116 @@
import type {
ExportFieldMeta,
ExportGranularity,
} from '#/views/business/order/product-order/config/exportFields';
import { getExportFields } from '#/views/business/order/product-order/config/exportFields';
/** 「金额」分组字段 key保持 exportFields 定义顺序 */
export function getAmountFieldKeys(granularity: ExportGranularity): string[] {
return getExportFields(granularity)
.filter((f) => f.group === '金额')
.map((f) => f.key);
}
/** 全量字段 canonical 顺序 */
export function getCanonicalFieldOrder(granularity: ExportGranularity): string[] {
return getExportFields(granularity).map((f) => f.key);
}
/**
* 将已选金额字段聚合为连续块,块内按金额组定义序排列。
* 块插入位置:原序中第一个已选金额字段的位置。
*/
export function clusterAmountFields(
keys: string[],
granularity: ExportGranularity,
): string[] {
const amountKeys = getAmountFieldKeys(granularity);
const amountSet = new Set(amountKeys);
const selectedAmount = amountKeys.filter((k) => keys.includes(k));
if (selectedAmount.length <= 1) {
return [...keys];
}
const nonAmount = keys.filter((k) => !amountSet.has(k));
let firstAmountIdx = keys.length;
for (let i = 0; i < keys.length; i++) {
if (amountSet.has(keys[i]!)) {
firstAmountIdx = i;
break;
}
}
let nonAmountBefore = 0;
for (let i = 0; i < firstAmountIdx; i++) {
if (!amountSet.has(keys[i]!)) {
nonAmountBefore++;
}
}
const result = [...nonAmount];
result.splice(nonAmountBefore, 0, ...selectedAmount);
return result;
}
/** canonical 过滤 + 费用聚合 */
export function buildDefaultOrderedKeys(
selectedKeys: string[],
granularity: ExportGranularity,
): string[] {
const selectedSet = new Set(selectedKeys);
const ordered = getCanonicalFieldOrder(granularity).filter((k) =>
selectedSet.has(k),
);
return clusterAmountFields(ordered, granularity);
}
/**
* 勾选变更时同步有序列表。
* customized=false重建默认序含费用聚合
* customized=true仅增删新增字段按 canonical 位置插入
*/
export function syncOrderedSelectedKeys(
selectedKeys: string[],
prevOrdered: string[],
granularity: ExportGranularity,
customized: boolean,
): string[] {
const selectedSet = new Set(selectedKeys);
if (!customized) {
return buildDefaultOrderedKeys(selectedKeys, granularity);
}
let ordered = prevOrdered.filter((k) => selectedSet.has(k));
const orderedSet = new Set(ordered);
const added = selectedKeys.filter((k) => !orderedSet.has(k));
if (added.length === 0) {
return ordered;
}
const canonical = getCanonicalFieldOrder(granularity);
for (const key of added) {
const canonicalIdx = canonical.indexOf(key);
let insertAt = ordered.length;
for (let i = 0; i < ordered.length; i++) {
const existingIdx = canonical.indexOf(ordered[i]!);
if (existingIdx > canonicalIdx) {
insertAt = i;
break;
}
}
ordered = [...ordered.slice(0, insertAt), key, ...ordered.slice(insertAt)];
}
return ordered;
}
export function formatFieldOrderPreview(
keys: string[],
fieldMeta: ExportFieldMeta[],
): string {
const labelMap = new Map(fieldMeta.map((f) => [f.key, f.label]));
return keys.map((k) => labelMap.get(k) ?? k).join(' → ');
}

View File

@@ -0,0 +1,403 @@
import ExcelJS from 'exceljs';
import type {
ExportFieldMeta,
ExportGranularity,
} from '#/views/business/order/product-order/config/exportFields';
import { granularityLabel } from '#/views/business/order/product-order/config/exportFields';
export interface OrderExportFilterInfo {
search_time: [string, string];
order_no?: string;
store_name?: string;
status_text?: string;
delivery_method_text?: string;
prescription_type_text?: string;
export_fields_text?: string;
row_count?: number;
}
export interface ExportProductOrderExcelOptions {
rows: Record<string, unknown>[];
selectedFields: string[];
fieldMeta: ExportFieldMeta[];
filters: OrderExportFilterInfo;
granularity: ExportGranularity;
exportedAt?: Date;
}
const THIN_BORDER: Partial<ExcelJS.Borders> = {
top: { style: 'thin', color: { argb: 'FF000000' } },
left: { style: 'thin', color: { argb: 'FF000000' } },
bottom: { style: 'thin', color: { argb: 'FF000000' } },
right: { style: 'thin', color: { argb: 'FF000000' } },
};
const TEXT_KEYS = new Set(['order_no', 'express_mobile', 'epl_order_no']);
function alignOf(meta: ExportFieldMeta): Partial<ExcelJS.Alignment> {
const horizontal =
meta.align === 'right'
? 'right'
: meta.align === 'left'
? 'left'
: 'center';
return { horizontal, vertical: 'middle', wrapText: meta.format === 'text' };
}
function cellValue(row: Record<string, unknown>, key: string, format: ExportFieldMeta['format']) {
const raw = row[key];
if (raw === null || raw === undefined || raw === '') {
return '';
}
if (format === 'currency' || format === 'integer') {
const num = Number(raw);
return Number.isFinite(num) ? num : raw;
}
return String(raw);
}
function applyCellFormat(cell: ExcelJS.Cell, meta: ExportFieldMeta) {
cell.alignment = alignOf(meta);
cell.border = THIN_BORDER;
if (TEXT_KEYS.has(meta.key)) {
cell.numFmt = '@';
} else if (meta.format === 'currency') {
cell.numFmt = '#,##0.00';
} else if (meta.format === 'datetime') {
cell.numFmt = 'yyyy-mm-dd hh:mm:ss';
} else if (meta.format === 'integer') {
cell.numFmt = '0';
}
}
function filterRows(filters: OrderExportFilterInfo): {
pairs: [[string, string], [string, string]][];
exportFields: [string, string];
rowCount: [string, string];
} {
const [start, end] = filters.search_time;
return {
pairs: [
[
['时间范围', `${start} ~ ${end}`],
['订单号', filters.order_no?.trim() || '全部'],
],
[
['门店', filters.store_name?.trim() || '全部'],
['订单状态', filters.status_text?.trim() || '全部'],
],
[
['发货方式', filters.delivery_method_text?.trim() || '全部'],
['订单类型', filters.prescription_type_text?.trim() || '全部'],
],
],
exportFields: ['导出字段', filters.export_fields_text || '—'],
rowCount: [
'数据条数',
filters.row_count != null ? `${filters.row_count}` : '—',
],
};
}
function styleFilterLabel(cell: ExcelJS.Cell, label: string) {
cell.value = label;
cell.font = { bold: true, size: 10 };
cell.fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'FFE8F0FE' },
};
cell.alignment = { horizontal: 'right', vertical: 'middle' };
cell.border = THIN_BORDER;
}
function styleFilterValue(cell: ExcelJS.Cell, value: string, wrapText = false) {
cell.value = value;
cell.font = { size: 10 };
cell.alignment = { horizontal: 'left', vertical: 'middle', wrapText };
cell.border = THIN_BORDER;
}
/** 一行两个条件:左/右各一组 label|valueright 省略时右侧合并留空 */
function renderFilterPairRow(
sheet: ExcelJS.Worksheet,
row: number,
layoutCols: number,
left: [string, string],
right?: [string, string],
) {
const mid = Math.floor(layoutCols / 2);
const leftValueEnd = mid;
const rightLabelCol = mid + 1;
const rightValueStart = mid + 2;
styleFilterLabel(sheet.getCell(row, 1), left[0]);
if (leftValueEnd >= 2) {
sheet.mergeCells(row, 2, row, leftValueEnd);
}
styleFilterValue(sheet.getCell(row, 2), left[1]);
if (right?.[0]) {
styleFilterLabel(sheet.getCell(row, rightLabelCol), right[0]);
if (rightValueStart <= layoutCols) {
if (rightValueStart < layoutCols) {
sheet.mergeCells(row, rightValueStart, row, layoutCols);
}
styleFilterValue(sheet.getCell(row, rightValueStart), right[1] ?? '');
}
} else if (rightLabelCol <= layoutCols) {
sheet.mergeCells(row, rightLabelCol, row, layoutCols);
const emptyCell = sheet.getCell(row, rightLabelCol);
emptyCell.border = THIN_BORDER;
}
sheet.getRow(row).height = 20;
}
/** 单行条件:标签 + 值占满剩余列 */
function renderFilterFullRow(
sheet: ExcelJS.Worksheet,
row: number,
layoutCols: number,
label: string,
value: string,
rowHeight = 20,
) {
styleFilterLabel(sheet.getCell(row, 1), label);
if (layoutCols >= 2) {
sheet.mergeCells(row, 2, row, layoutCols);
}
styleFilterValue(sheet.getCell(row, 2), value, true);
sheet.getRow(row).height = rowHeight;
}
function renderFilterBlock(
sheet: ExcelJS.Worksheet,
filters: OrderExportFilterInfo,
startRow: number,
layoutCols: number,
): number {
const { pairs, exportFields, rowCount } = filterRows(filters);
let r = startRow;
pairs.forEach(([left, right]) => {
renderFilterPairRow(sheet, r, layoutCols, left, right);
r += 1;
});
renderFilterFullRow(sheet, r, layoutCols, exportFields[0], exportFields[1], 28);
r += 1;
renderFilterPairRow(sheet, r, layoutCols, rowCount);
r += 1;
return r;
}
function autoFitColumns(
sheet: ExcelJS.Worksheet,
colCount: number,
fieldMeta: ExportFieldMeta[],
dataStartRow: number,
dataEndRow: number,
) {
for (let c = 1; c <= colCount; c++) {
const meta = fieldMeta[c - 1];
let maxLen = meta?.label.length ?? 8;
for (let r = dataStartRow; r <= dataEndRow; r++) {
const cell = sheet.getCell(r, c);
const v = cell.value?.toString() ?? '';
maxLen = Math.max(maxLen, v.length);
}
const width = Math.min(Math.max(maxLen * 1.15 + 2, meta?.width ?? 10), 55);
sheet.getColumn(c).width = width;
}
}
export async function exportProductOrderExcel(
options: ExportProductOrderExcelOptions,
): Promise<ArrayBuffer> {
const {
rows,
selectedFields,
fieldMeta,
filters,
granularity,
exportedAt = new Date(),
} = options;
const columns = selectedFields
.map((key) => fieldMeta.find((f) => f.key === key))
.filter((f): f is ExportFieldMeta => !!f);
if (columns.length === 0) {
throw new Error('请至少选择一个导出字段');
}
const workbook = new ExcelJS.Workbook();
workbook.creator = '萧康云医';
const sheetName = granularity === 'item' ? '商品明细' : '商品订单';
const sheet = workbook.addWorksheet(sheetName, {
views: [{ showGridLines: true }],
});
const colCount = columns.length;
const layoutCols = Math.max(colCount, 4);
// Row 1: title
sheet.mergeCells(1, 1, 1, layoutCols);
const titleCell = sheet.getCell(1, 1);
titleCell.value = `萧康云医 · 商品订单导出报表(${granularityLabel(granularity)}`;
titleCell.font = { bold: true, size: 16, color: { argb: 'FF1F4E79' } };
titleCell.fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'FFF5F7FA' },
};
titleCell.alignment = { horizontal: 'center', vertical: 'middle' };
titleCell.border = THIN_BORDER;
sheet.getRow(1).height = 36;
// Row 2: meta
const metaSplit = Math.max(1, Math.floor(layoutCols / 2));
sheet.mergeCells(2, 1, 2, metaSplit);
sheet.mergeCells(2, metaSplit + 1, 2, layoutCols);
const leftMeta = sheet.getCell(2, 1);
leftMeta.value = `导出时间:${formatDateTime(exportedAt)}`;
leftMeta.font = { size: 10 };
leftMeta.alignment = { horizontal: 'left', vertical: 'middle' };
const rightMeta = sheet.getCell(2, metaSplit + 1);
rightMeta.value = `导出粒度:${granularityLabel(granularity)}`;
rightMeta.font = { size: 10 };
rightMeta.alignment = { horizontal: 'right', vertical: 'middle' };
sheet.getRow(2).height = 22;
// Filter block: 2 conditions per row (4-col layout minimum)
const filterStartRow = 3;
const afterFilterRow = renderFilterBlock(sheet, filters, filterStartRow, layoutCols);
const headerRowNum = afterFilterRow + 1;
sheet.getRow(afterFilterRow).height = 8;
// Header row
columns.forEach((meta, i) => {
const cell = sheet.getCell(headerRowNum, i + 1);
cell.value = meta.label;
cell.font = { bold: true, color: { argb: 'FF000000' } };
cell.fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'FFFFFF00' },
};
cell.alignment = { horizontal: 'center', vertical: 'middle' };
cell.border = THIN_BORDER;
});
sheet.getRow(headerRowNum).height = 30;
const dataStartRow = headerRowNum + 1;
rows.forEach((row, rowIdx) => {
const excelRow = sheet.getRow(dataStartRow + rowIdx);
columns.forEach((meta, colIdx) => {
const cell = excelRow.getCell(colIdx + 1);
cell.value = cellValue(row, meta.key, meta.format) as ExcelJS.CellValue;
applyCellFormat(cell, meta);
if (rowIdx % 2 === 1) {
cell.fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'FFF2F2F2' },
};
}
});
excelRow.height = 22;
});
const dataEndRow = dataStartRow + Math.max(rows.length, 1) - 1;
// Summary row (order granularity)
let summaryRowNum = dataEndRow + 1;
if (granularity === 'order' && rows.length > 0) {
const summaryFields = columns.filter((c) => c.summary);
if (summaryFields.length > 0) {
summaryRowNum = dataEndRow + 2;
sheet.mergeCells(summaryRowNum, 1, summaryRowNum, Math.min(2, colCount));
const summaryTitle = sheet.getCell(summaryRowNum, 1);
summaryTitle.value = '汇总';
summaryTitle.font = { bold: true, size: 11 };
summaryTitle.fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'FFFFF9E6' },
};
summaryTitle.border = {
...THIN_BORDER,
top: { style: 'double', color: { argb: 'FF000000' } },
};
columns.forEach((meta, colIdx) => {
const cell = sheet.getCell(summaryRowNum, colIdx + 1);
cell.border = {
...THIN_BORDER,
top: { style: 'double', color: { argb: 'FF000000' } },
};
cell.fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'FFFFF9E6' },
};
cell.font = { bold: true };
if (colIdx === 0) {
cell.value = '汇总';
} else if (meta.summary) {
const sum = rows.reduce((acc, row) => {
const n = Number(row[meta.key]);
return acc + (Number.isFinite(n) ? n : 0);
}, 0);
cell.value = sum;
applyCellFormat(cell, meta);
}
});
sheet.getRow(summaryRowNum).height = 24;
}
}
autoFitColumns(sheet, colCount, columns, headerRowNum, dataEndRow);
sheet.autoFilter = {
from: { row: headerRowNum, column: 1 },
to: { row: dataEndRow, column: colCount },
};
sheet.views = [
{
state: 'frozen',
xSplit: 0,
ySplit: headerRowNum,
topLeftCell: `A${headerRowNum + 1}`,
activeCell: `A${headerRowNum + 1}`,
},
];
sheet.pageSetup = {
orientation: 'landscape',
fitToPage: true,
fitToWidth: 1,
fitToHeight: 0,
};
return workbook.xlsx.writeBuffer();
}
function formatDateTime(d: Date): string {
const pad = (n: number) => String(n).padStart(2, '0');
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
}
export function exportFilename(
granularity: ExportGranularity,
searchTime: [string, string],
): string {
const g = granularity === 'item' ? '明细' : '订单';
return `萧康云医-商品订单_${g}_${searchTime[0]}_${searchTime[1]}.xlsx`;
}

View File

@@ -0,0 +1,85 @@
import type { ExportGranularity } from '#/views/business/order/product-order/config/exportFields';
import { getExportFields } from '#/views/business/order/product-order/config/exportFields';
import { buildDefaultOrderedKeys } from '#/views/business/order/product-order/utils/exportFieldOrder';
export interface ExportSchemeConfig {
order_no?: string;
store_id?: number | null;
status?: number | null;
delivery_method?: number | null;
prescription_type?: number | null;
/** 有序导出字段 key 列表 */
selected_fields: string[];
}
export interface ExportSchemeItem {
id: number;
name: string;
is_default: number;
granularity: ExportGranularity;
config: ExportSchemeConfig;
created_at?: string;
updated_at?: string;
}
export interface ExportFormState {
orderNo: string;
storeId: number | undefined;
status: number | undefined;
deliveryMethod: number | undefined;
prescriptionType: number | undefined;
granularity: ExportGranularity;
selectedKeys: string[];
orderedSelectedKeys: string[];
}
export function buildSchemeConfigFromForm(form: ExportFormState): ExportSchemeConfig {
return {
order_no: form.orderNo.trim(),
store_id: form.storeId ?? null,
status: form.status ?? null,
delivery_method: form.deliveryMethod ?? null,
prescription_type: form.prescriptionType ?? null,
selected_fields: [...form.orderedSelectedKeys],
};
}
export function sanitizeSelectedFields(
granularity: ExportGranularity,
keys: string[],
): string[] {
const allowed = new Set(getExportFields(granularity).map((f) => f.key));
return keys.filter((k) => allowed.has(k));
}
export function applySchemeToForm(
scheme: ExportSchemeItem,
form: ExportFormState,
): void {
const cfg = scheme.config ?? ({} as ExportSchemeConfig);
form.granularity = scheme.granularity;
form.orderNo = cfg.order_no?.trim() ?? '';
form.storeId = cfg.store_id ?? undefined;
form.status = cfg.status ?? undefined;
form.deliveryMethod = cfg.delivery_method ?? undefined;
form.prescriptionType = cfg.prescription_type ?? undefined;
const fields = sanitizeSelectedFields(
scheme.granularity,
cfg.selected_fields ?? [],
);
if (fields.length > 0) {
form.orderedSelectedKeys = fields;
form.selectedKeys = [...fields];
} else {
const defaults = getExportFields(scheme.granularity)
.filter((f) => f.default)
.map((f) => f.key);
form.selectedKeys = defaults;
form.orderedSelectedKeys = buildDefaultOrderedKeys(defaults, scheme.granularity);
}
}
export function findDefaultScheme(schemes: ExportSchemeItem[]): ExportSchemeItem | undefined {
return schemes.find((s) => s.is_default === 1);
}

View File

@@ -133,10 +133,13 @@ const openBankCardReport = async (row: any, update = false) => {
} catch {
report = null;
}
const reportStatus = Number(report?.report_status ?? 0);
const useUpdate =
(update || [3, 4].includes(reportStatus)) && Boolean(report?.id);
bankCardReportModalApi.setData({
storeId: row.id,
storeName: row.name,
mode: update && report?.id ? 'update' : 'report',
mode: useUpdate ? 'update' : 'report',
report,
gridApi,
});

View File

@@ -1,186 +1,182 @@
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { Button, Input, InputNumber, Popover, Tag } from 'ant-design-vue';
type TagOption = { label: string; value: number | string };
const props = withDefaults(
defineProps<{
disabled?: boolean;
displayFormatter?: (value: unknown) => string;
label?: string;
options?: TagOption[];
placeholder?: string;
type?: 'number' | 'tags' | 'text';
value?: null | number | string;
}>(),
{
disabled: false,
label: '',
options: () => [],
placeholder: '请输入',
type: 'text',
value: '',
},
);
const emit = defineEmits<{
save: [value: number | string];
}>();
const open = ref(false);
const draftText = ref('');
const draftNumber = ref<null | number>(null);
const saving = ref(false);
watch(open, (visible) => {
if (!visible) {
return;
}
if (props.type === 'number') {
const raw = props.value;
draftNumber.value =
raw !== null && raw !== undefined && raw !== '' ? Number(raw) : null;
return;
}
draftText.value = String(props.value ?? '');
});
function displayText() {
if (props.displayFormatter) {
return props.displayFormatter(props.value);
}
if (props.value === null || props.value === undefined || props.value === '') {
return '-';
}
if (props.type === 'tags') {
const matched = props.options.find(
(item) => String(item.value) === String(props.value),
);
return matched?.label ?? String(props.value);
}
return String(props.value);
}
async function confirmSave() {
saving.value = true;
try {
if (props.type === 'number') {
if (draftNumber.value === null || draftNumber.value === undefined) {
return;
}
emit('save', draftNumber.value);
} else {
const text = draftText.value.trim();
if (!text) {
return;
}
emit('save', text);
}
open.value = false;
} finally {
saving.value = false;
}
}
function selectTag(value: number | string) {
emit('save', value);
open.value = false;
}
</script>
<template>
<Popover v-model:open="open" trigger="click" placement="bottomLeft">
<template #content>
<div class="inline-editor-popover">
<div v-if="label" class="inline-editor-popover__label">{{ label }}</div>
<template v-if="type === 'tags'">
<div class="inline-editor-popover__tags">
<Tag
v-for="opt in options"
:key="String(opt.value)"
class="cursor-pointer"
:color="String(value) === String(opt.value) ? 'processing' : 'default'"
@click="selectTag(opt.value)"
>
{{ opt.label }}
</Tag>
</div>
</template>
<template v-else-if="type === 'number'">
<InputNumber
v-model:value="draftNumber"
class="w-full"
:min="0"
:placeholder="placeholder"
/>
<div class="inline-editor-popover__actions">
<Button size="small" @click="open = false">取消</Button>
<Button
size="small"
type="primary"
:loading="saving"
@click="confirmSave"
>
保存
</Button>
</div>
</template>
<template v-else>
<Input
v-model:value="draftText"
:placeholder="placeholder"
@press-enter="confirmSave"
/>
<div class="inline-editor-popover__actions">
<Button size="small" @click="open = false">取消</Button>
<Button
size="small"
type="primary"
:loading="saving"
@click="confirmSave"
>
保存
</Button>
</div>
</template>
</div>
</template>
<Button
class="h-auto p-0 text-left"
:disabled="disabled"
size="small"
type="link"
@click.stop
>
{{ displayText() }}
</Button>
</Popover>
</template>
<style scoped>
.inline-editor-popover {
min-width: 200px;
}
.inline-editor-popover__label {
margin-bottom: 8px;
font-size: 12px;
color: #6b7280;
}
.inline-editor-popover__tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
max-width: 280px;
}
.inline-editor-popover__actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 12px;
}
</style>
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { Button, Input, InputNumber, Popover, Tag } from 'ant-design-vue';
type TagOption = { label: string; value: number | string };
const props = withDefaults(
defineProps<{
disabled?: boolean;
displayFormatter?: (value: unknown) => string;
label?: string;
options?: TagOption[];
placeholder?: string;
type?: 'number' | 'tags' | 'text';
value?: null | number | string;
}>(),
{
disabled: false,
label: '',
options: () => [],
placeholder: '请输入',
type: 'text',
value: '',
},
);
const emit = defineEmits<{
save: [value: number | string];
}>();
const open = ref(false);
const draftText = ref('');
const draftNumber = ref<null | number>(null);
const saving = ref(false);
watch(open, (visible) => {
if (!visible) {
return;
}
if (props.type === 'number') {
const raw = props.value;
draftNumber.value =
raw !== null && raw !== undefined && raw !== '' ? Number(raw) : null;
return;
}
draftText.value = String(props.value ?? '');
});
function displayText() {
if (props.displayFormatter) {
return props.displayFormatter(props.value);
}
if (props.value === null || props.value === undefined || props.value === '') {
return '点击设置';
}
if (props.type === 'tags') {
const matched = props.options.find(
(item) => String(item.value) === String(props.value),
);
return matched?.label ?? String(props.value);
}
return String(props.value);
}
function isEmptyValue() {
return props.value === null || props.value === undefined || props.value === '';
}
async function confirmSave() {
saving.value = true;
try {
if (props.type === 'number') {
if (draftNumber.value === null || draftNumber.value === undefined) {
return;
}
emit('save', draftNumber.value);
} else {
const text = draftText.value.trim();
if (!text) {
return;
}
emit('save', text);
}
open.value = false;
} finally {
saving.value = false;
}
}
function selectTag(value: number | string) {

View File

@@ -1,118 +1,220 @@
<script lang="ts" setup>
import { message } from 'ant-design-vue';
import { Descriptions, Empty, message } from 'ant-design-vue';
import { updateProcess } from '#/views/system/process/api';
import {
CALC_METHOD_MAP,
CALC_METHOD_OPTIONS,
UNIT_OPTIONS,
} from '../constants';
import type { ProcessRuleRow } from '../constants';
import InlineFieldEditor from './inline-field-editor.vue';
import ConfigSectionHeader from '#/views/system/shared/config-section-header.vue';
const props = defineProps<{
method: ProcessRuleRow | null | undefined;
}>();
const emit = defineEmits<{
updated: [patch: Partial<ProcessRuleRow>];
}>();
async function saveField(payload: Record<string, unknown>) {
if (!props.method) {
return;
}
await updateProcess({
id: props.method.id,
node_type: 'rule',
...payload,
});
emit('updated', payload as Partial<ProcessRuleRow>);
message.success('保存成功');
}
</script>
<template>
<div v-if="method" class="method-info-bar">
<div class="method-info-bar__item">
<span class="method-info-bar__label">级别</span>
<span>煎法</span>
<section class="rounded-lg border border-border bg-muted/30 dark:border-gray-700">
<ConfigSectionHeader class="mb-0 px-4 pt-3" title="煎法配置" />
<div v-if="method" class="px-4 pb-3 pt-1">
<Descriptions
bordered
:column="3"
size="small"
class="process-method-descriptions"
>
<Descriptions.Item label="级别">煎法</Descriptions.Item>
<Descriptions.Item label="名称">
<InlineFieldEditor
:value="method.name"
label="名称"
placeholder="请输入名称"
@save="(val) => saveField({ name: val })"
/>
</Descriptions.Item>
<Descriptions.Item label="计算方式">
<InlineFieldEditor
type="tags"
:value="method.calc_method"
label="计算方式"
:options="[...CALC_METHOD_OPTIONS]"
:display-formatter="
(val) => CALC_METHOD_MAP[Number(val)] || String(val ?? '点击设置')
"
@save="(val) => saveField({ calc_method: Number(val) })"
/>
</Descriptions.Item>
<Descriptions.Item label="计算价格">
<InlineFieldEditor
type="number"
:value="method.price"
label="计算价格"
placeholder="请输入价格"
@save="(val) => saveField({ price: val })"
/>
</Descriptions.Item>
<Descriptions.Item label="计算单位" :span="2">
<InlineFieldEditor
type="tags"
:value="method.unit"
label="计算单位"
:options="UNIT_OPTIONS.map((u) => ({ label: u, value: u }))"
@save="(val) => saveField({ unit: val })"
/>
</Descriptions.Item>
</Descriptions>
</div>
<div class="method-info-bar__item">
<span class="method-info-bar__label">名称</span>
<InlineFieldEditor
:value="method.name"
label="名称"
placeholder="请输入名称"
@save="(val) => saveField({ name: val })"
/>
<div v-else class="flex justify-center px-4 py-6">
<Empty description="请选择上方煎法 Tab" />
</div>
<div class="method-info-bar__item">
<span class="method-info-bar__label">计算方式</span>
<InlineFieldEditor
type="tags"
:value="method.calc_method"
label="计算方式"
:options="[...CALC_METHOD_OPTIONS]"
:display-formatter="
(val) => CALC_METHOD_MAP[Number(val)] || String(val ?? '-')
"
@save="(val) => saveField({ calc_method: Number(val) })"
/>
</div>
<div class="method-info-bar__item">
<span class="method-info-bar__label">计算价格</span>
<InlineFieldEditor
type="number"
:value="method.price"
label="计算价格"
placeholder="请输入价格"
@save="(val) => saveField({ price: val })"
/>
</div>
<div class="method-info-bar__item">
<span class="method-info-bar__label">计算单位</span>
<InlineFieldEditor
type="tags"
:value="method.unit"
label="计算单位"
:options="UNIT_OPTIONS.map((u) => ({ label: u, value: u }))"
@save="(val) => saveField({ unit: val })"
/>
</div>
</div>
<div v-else class="method-info-bar method-info-bar--empty">
请选择或新增煎法
</div>
</section>
</template>
<style scoped>
.method-info-bar {
display: flex;
flex-wrap: wrap;
gap: 16px 24px;
padding: 12px 16px;
margin-bottom: 12px;
background: #fafafa;
border: 1px solid #f0f0f0;
border-radius: 8px;
.process-method-descriptions :deep(.ant-descriptions-view) {
border-color: hsl(var(--border));
}
.method-info-bar--empty {
color: #9ca3af;
.process-method-descriptions :deep(.ant-descriptions-item-label) {
color: hsl(var(--muted-foreground));
background: hsl(var(--muted) / 0.3);
}
.method-info-bar__item {
display: flex;
gap: 8px;
align-items: center;
min-width: 140px;
.process-method-descriptions :deep(.ant-descriptions-item-content) {
color: hsl(var(--foreground));
background: hsl(var(--card));
}
.method-info-bar__label {
flex-shrink: 0;
font-size: 13px;
color: #6b7280;
}
</style>

View File

@@ -3,6 +3,10 @@ import { computed, ref } from 'vue';
import { getProcessList } from '#/views/system/process/api';
import type { ProcessNoteRow, ProcessRuleRow } from '../constants';
import {
readProcessSelection,
writeProcessSelection,
} from '../utils/processSelectionStorage';
export function parseProcessListItems(items: Record<string, any>[]) {
const level1: ProcessRuleRow[] = [];
@@ -47,14 +51,35 @@ export function parseProcessListItems(items: Record<string, any>[]) {
return { level1, level2Map, notesMap };
}
function restoreSelection(
level1: ProcessRuleRow[],
level2Map: Record<number, ProcessRuleRow[]>,
savedLevel1Id: number | null,
savedMethodId: number | null,
) {
let level1Id = savedLevel1Id;
if (!level1Id || !level1.some((item) => item.id === level1Id)) {
level1Id = level1[0]?.id ?? null;
}
const methods = level1Id ? (level2Map[level1Id] ?? []) : [];
let methodId = savedMethodId;
if (!methodId || !methods.some((item) => item.id === methodId)) {
methodId = methods[0]?.id ?? null;
}
return { level1Id, methodId };
}
export function useProcessTree() {
const saved = readProcessSelection();
const loading = ref(false);
const keyword = ref('');
const level1List = ref<ProcessRuleRow[]>([]);
const level2Map = ref<Record<number, ProcessRuleRow[]>>({});
const notesMap = ref<Record<number, ProcessNoteRow[]>>({});
const selectedLevel1Id = ref<number | null>(null);
const selectedMethodId = ref<number | null>(null);
const selectedLevel1Id = ref<number | null>(saved.level1Id);
const selectedMethodId = ref<number | null>(saved.methodId);
const filteredLevel1 = computed(() => {
const kw = keyword.value.trim().toLowerCase();
@@ -103,6 +128,13 @@ export function useProcessTree() {
level1List.value.find((item) => item.id === selectedLevel1Id.value),
);
function persistSelection() {
writeProcessSelection({
level1Id: selectedLevel1Id.value,
methodId: selectedMethodId.value,
});
}
async function loadData() {
loading.value = true;
try {
@@ -116,22 +148,15 @@ export function useProcessTree() {
level2Map.value = parsed.level2Map;
notesMap.value = parsed.notesMap;
if (
!selectedLevel1Id.value ||
!level1List.value.some((item) => item.id === selectedLevel1Id.value)
) {
selectedLevel1Id.value = level1List.value[0]?.id ?? null;
}
const methods = selectedLevel1Id.value
? (level2Map.value[selectedLevel1Id.value] ?? [])
: [];
if (
!selectedMethodId.value ||
!methods.some((item) => item.id === selectedMethodId.value)
) {
selectedMethodId.value = methods[0]?.id ?? null;
}
const restored = restoreSelection(
level1List.value,
level2Map.value,
selectedLevel1Id.value,
selectedMethodId.value,
);
selectedLevel1Id.value = restored.level1Id;
selectedMethodId.value = restored.methodId;
persistSelection();
} finally {
loading.value = false;
}
@@ -141,10 +166,12 @@ export function useProcessTree() {
selectedLevel1Id.value = id;
const methods = level2Map.value[id] ?? [];
selectedMethodId.value = methods[0]?.id ?? null;
persistSelection();
}
function selectMethod(id: number) {
selectedMethodId.value = id;
persistSelection();
}
function patchRule(id: number, patch: Partial<ProcessRuleRow>) {

View File

@@ -1,5 +1,7 @@
import type { VxeGridProps } from '#/adapter/vxe-table';
import { SUB_LIST_GRID_HEIGHT } from '#/views/system/shared/sub-list-grid';
export const noteGridOptions: VxeGridProps = {
columnConfig: {
useKey: true,
@@ -33,7 +35,7 @@ export const noteGridOptions: VxeGridProps = {
minWidth: 160,
},
],
height: 'auto',
height: SUB_LIST_GRID_HEIGHT,
border: true,
showOverflow: true,
pagerConfig: {

View File

@@ -1,16 +1,9 @@
<script lang="ts" setup>
import { onMounted, watch } from 'vue';
import { computed, onMounted, watch } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import {
Button,
Empty,
Input,
Spin,
Tabs,
message,
} from 'ant-design-vue';
import { Button, Empty, Tabs, message } from 'ant-design-vue';
import { PlusOutlined } from '@ant-design/icons-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
@@ -22,6 +15,9 @@ import InlineFieldEditor from './components/inline-field-editor.vue';
import MethodInfoBar from './components/method-info-bar.vue';
import FormModalDemo from './components/modal.vue';
import MovePidModal from './components/move-pid-modal.vue';
import ConfigSectionHeader from '#/views/system/shared/config-section-header.vue';
import ConfigTreeSidebar from '#/views/system/shared/config-tree-sidebar.vue';
import type { TreeSidebarRow } from '#/views/system/shared/types';
import { useProcessTree } from './composables/useProcessTree';
import type { ProcessNoteRow, ProcessRuleRow } from './constants';
import { noteGridOptions } from './config/note-table';
@@ -45,6 +41,10 @@ const {
selectedMethodId,
} = useProcessTree();
const breadcrumbMethodName = computed(
() => selectedMethod.value?.name ?? '未选择煎法',
);
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: FormModalDemo,
});
@@ -195,8 +195,8 @@ function buildNoteMenus(row: ProcessNoteRow): ContextMenuItem[] {
];
}
function onLevel1ContextMenu(e: MouseEvent, row: ProcessRuleRow) {
showContextMenu(e, buildLevel1Menus(row), row);
function onLevel1ContextMenu(e: MouseEvent, row: TreeSidebarRow) {
showContextMenu(e, buildLevel1Menus(row as ProcessRuleRow), row);
}
function onMethodTabContextMenu(e: MouseEvent, row: ProcessRuleRow) {
@@ -207,9 +207,10 @@ function onNoteContextMenu(e: MouseEvent, row: ProcessNoteRow) {
showContextMenu(e, buildNoteMenus(row), row);
}
async function saveLevel1Name(row: ProcessRuleRow, name: string) {
await updateProcess({ id: row.id, node_type: 'rule', name });
patchRule(row.id, { name });
async function saveLevel1Name(row: TreeSidebarRow, name: string) {
const rule = row as ProcessRuleRow;
await updateProcess({ id: rule.id, node_type: 'rule', name });
patchRule(rule.id, { name });
message.success('保存成功');
}
@@ -243,114 +244,198 @@ function onSelectLevel1(id: number) {
selectLevel1(id);
syncNoteGrid();
}
function openCreateMethod() {
if (!selectedLevel1.value) {
return;
}
openCreateModal({ formLevel: 2, parentRow: selectedLevel1.value });
}
function openCreateNote() {
if (!selectedMethod.value) {
return;
}
openCreateModal({ formLevel: 3, parentRow: selectedMethod.value });
}
</script>
<template>
<Page auto-content-height title="加工费管理">
<Page
auto-content-height
description="右键制剂、煎法 Tab 或备注行可新增/移动;点击虚线下划线字段可编辑"
title="加工费管理"
>
<FormModal />
<MoveModal />
<div class="process-layout">
<aside class="process-sidebar">
<div class="process-sidebar__toolbar">
<Button type="primary" @click="openCreateModal({ formLevel: 1 })">
<PlusOutlined />
新增制剂
</Button>
</div>
<Input
v-model:value="keyword"
allow-clear
class="process-sidebar__search"
placeholder="搜索名称"
/>
<Spin :spinning="loading">
<div v-if="filteredLevel1.length" class="process-sidebar__list">
<div
v-for="item in filteredLevel1"
:key="item.id"
class="process-sidebar__item"
:class="{ active: selectedLevel1Id === item.id }"
@click="onSelectLevel1(item.id)"
@contextmenu="onLevel1ContextMenu($event, item)"
>
<InlineFieldEditor
:value="item.name"
@click.stop
@save="(val) => saveLevel1Name(item, String(val))"
/>
</div>
</div>
<Empty v-else description="暂无制剂" />
</Spin>
</aside>
<div class="flex h-full min-h-0 gap-4">
<ConfigTreeSidebar
add-label="新增制剂"
empty-text="暂无制剂"
:filtered-level1="filteredLevel1"
:keyword="keyword"
:loading="loading"
search-placeholder="搜索制剂名称"
:selected-level1-id="selectedLevel1Id"
title="制剂"
:total-count="level1List.length"
@add-level1="openCreateModal({ formLevel: 1 })"
@contextmenu="onLevel1ContextMenu"
@save-name="saveLevel1Name"
@select="onSelectLevel1"
@update:keyword="(val) => (keyword = val)"
/>
<main class="process-main">
<main
class="min-w-0 flex-1 rounded-lg border border-border bg-card px-4 py-3 dark:border-gray-700"
>
<template v-if="selectedLevel1">
<div class="process-main__header">
<span class="process-main__title">{{ selectedLevel1.name }}</span>
<Button
type="link"
@click="openCreateModal({ formLevel: 2, parentRow: selectedLevel1 })"
<div class="space-y-4">
<div
class="flex flex-wrap items-center justify-between gap-3 border-b border-border pb-3"
>
新增煎法
</Button>
</div>
<Tabs
v-if="currentMethods.length"
:active-key="String(selectedMethodId ?? '')"
type="card"
@change="onTabChange"
>
<Tabs.TabPane
v-for="method in currentMethods"
:key="String(method.id)"
>
<template #tab>
<div class="flex min-w-0 flex-wrap items-center gap-2 text-sm">
<span class="font-medium text-foreground">{{
selectedLevel1.name
}}</span>
<span class="text-muted-foreground">/</span>
<span
@contextmenu.prevent="onMethodTabContextMenu($event, method)"
>
{{ method.name }}
</span>
</template>
</Tabs.TabPane>
</Tabs>
<Empty
v-else
description="暂无煎法,请右键制剂或点击新增"
/>
<MethodInfoBar
:method="selectedMethod"
@updated="(patch) => selectedMethod && patchRule(selectedMethod.id, patch)"
/>
<NoteGrid @contextmenu.prevent>
<template #name="{ row }">
<span @contextmenu="onNoteContextMenu($event, row)">
<InlineFieldEditor
:value="row.name"
@save="(val) => saveNoteField(row, 'name', val)"
/>
</span>
</template>
<template #volume="{ row }">
<span @contextmenu="onNoteContextMenu($event, row)">
<InlineFieldEditor
type="number"
:value="row.volume"
:display-formatter="
(val) =>
val !== null && val !== undefined && val !== ''
? `${val}ml`
: '设置'
:class="
selectedMethod
? 'text-foreground'
: 'text-muted-foreground'
"
@save="(val) => saveNoteField(row, 'volume', val)"
/>
</span>
</template>
</NoteGrid>
>
{{ breadcrumbMethodName }}
</span>
</div>
<div class="flex shrink-0 flex-wrap items-center gap-2">
<Button size="small" @click="openCreateMethod">
<PlusOutlined />
新增煎法
</Button>
<Button
size="small"
type="primary"
:disabled="!selectedMethod"
@click="openCreateNote"
>
<PlusOutlined />
新增备注
</Button>
</div>
</div>
<section>
<Tabs
v-if="currentMethods.length"
:active-key="String(selectedMethodId ?? '')"
class="process-method-tabs select-none"
type="line"
@change="onTabChange"
>
<Tabs.TabPane
v-for="method in currentMethods"
:key="String(method.id)"
>
<template #tab>
<span
class="cursor-pointer"
@contextmenu.prevent="
onMethodTabContextMenu($event, method)
"
>
{{ method.name }}
</span>
</template>
</Tabs.TabPane>
</Tabs>
<div
v-else
class="flex flex-col items-center justify-center rounded-lg border border-dashed border-border py-8 dark:border-gray-700"
>
<Empty description="暂无煎法" />
<Button class="mt-4" type="primary" @click="openCreateMethod">
<PlusOutlined />
新增煎法
</Button>
</div>
</section>
<MethodInfoBar
:method="selectedMethod"
@updated="
(patch) => selectedMethod && patchRule(selectedMethod.id, patch)
"
/>
<section
class="rounded-lg border border-border dark:border-gray-700"
>
<ConfigSectionHeader
class="mb-3 px-4 pt-3"
:subtitle="selectedMethod ? `共 ${currentNotes.length} 条` : undefined"
title="备注列表"
>
<template #actions>
<Button
size="small"
type="primary"
:disabled="!selectedMethod"
@click="openCreateNote"
>
<PlusOutlined />
新增备注
</Button>
</template>
</ConfigSectionHeader>
<div
v-if="selectedMethod && currentNotes.length"
class="overflow-hidden rounded-b-lg border-t border-border dark:border-gray-700"
>
<NoteGrid @contextmenu.prevent>
<template #name="{ row }">
<span @contextmenu="onNoteContextMenu($event, row)">
<InlineFieldEditor
:value="row.name"
@save="(val) => saveNoteField(row, 'name', val)"
/>
</span>
</template>
<template #volume="{ row }">
<span @contextmenu="onNoteContextMenu($event, row)">
<InlineFieldEditor
type="number"
:value="row.volume"
:display-formatter="
(val) =>
val !== null && val !== undefined && val !== ''
? `${val}ml`
: '点击设置'
"
@save="(val) => saveNoteField(row, 'volume', val)"
/>
</span>
</template>
</NoteGrid>
</div>
<div
v-else-if="selectedMethod"
class="flex flex-col items-center justify-center px-4 py-8"
>
<Empty description="暂无备注" />
<Button class="mt-4" type="primary" @click="openCreateNote">
<PlusOutlined />
新增备注
</Button>
</div>
<div v-else class="px-4 pb-4">
<Empty description="请先选择煎法" />
</div>
</section>
</div>
</template>
<Empty v-else description="请选择左侧制剂" />
</main>
@@ -359,69 +444,11 @@ function onSelectLevel1(id: number) {
</template>
<style scoped>
.process-layout {
display: flex;
gap: 16px;
height: 100%;
min-height: 0;
.process-method-tabs :deep(.ant-tabs-nav) {
margin-bottom: 0;
}
.process-sidebar {
display: flex;
flex-shrink: 0;
flex-direction: column;
width: 220px;
padding: 12px;
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 8px;
}
.process-sidebar__toolbar {
margin-bottom: 12px;
}
.process-sidebar__search {
margin-bottom: 12px;
}
.process-sidebar__list {
display: flex;
flex-direction: column;
gap: 4px;
max-height: calc(100vh - 260px);
overflow: auto;
}
.process-sidebar__item {
padding: 8px 12px;
cursor: pointer;
border-radius: 6px;
}
.process-sidebar__item:hover,
.process-sidebar__item.active {
background: #f0fbf8;
}
.process-main {
flex: 1;
min-width: 0;
padding: 12px 16px;
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 8px;
}
.process-main__header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.process-main__title {
font-size: 16px;
font-weight: 600;
.process-method-tabs :deep(.ant-tabs-tab) {
transition: color 0.2s;
}
</style>

View File

@@ -0,0 +1,38 @@
const STORAGE_KEY = 'process-management-selection';
export interface ProcessSelectionState {
level1Id: number | null;
methodId: number | null;
}
export function readProcessSelection(): ProcessSelectionState {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) {
return { level1Id: null, methodId: null };
}
const parsed = JSON.parse(raw) as Partial<ProcessSelectionState>;
return {
level1Id:
parsed.level1Id != null && Number.isFinite(Number(parsed.level1Id))
? Number(parsed.level1Id)
: null,
methodId:
parsed.methodId != null && Number.isFinite(Number(parsed.methodId))
? Number(parsed.methodId)
: null,
};
} catch {
return { level1Id: null, methodId: null };
}
}
export function writeProcessSelection(state: ProcessSelectionState) {
localStorage.setItem(
STORAGE_KEY,
JSON.stringify({
level1Id: state.level1Id,
methodId: state.methodId,
}),
);
}

View File

@@ -9,13 +9,44 @@ import { useVbenForm } from '#/adapter/form';
import { createRegion, updateRegion } from '#/views/system/region/api';
import { modalFormProps } from '#/views/system/region/config/form';
const isUpdate = ref(false);
const gridApi = ref();
const onSuccessRef = ref<(() => void) | null>(null);
const [Form, formApi] = useVbenForm(modalFormProps);
function buildSubmitPayload(values: Record<string, any>) {
const formLevel = Number(values.formLevel);
return {
id: values.id,
name: values.name,
pid: formLevel === 1 ? 0 : values.pid,
express_fee: values.express_fee ?? 0,
};
}
function validateFormValues(values: Record<string, any>) {
const formLevel = Number(values.formLevel);
if (!String(values.name ?? '').trim()) {
message.error('请输入名称');
return false;
}
if (formLevel === 2 && !values.pid) {
message.error('请选择父级地区');
return false;
}
if (
values.express_fee === null ||
values.express_fee === undefined ||
values.express_fee === ''
) {
message.error('请输入快递费');
return false;
}
return true;
}
const modalTitle = ref('新增快递费');
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
draggable: true,
@@ -23,38 +54,47 @@ const [Modal, modalApi] = useVbenModal({
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 ? updateRegion : createRegion;
submitApi(values)
.then(() => {
message.success('保存成功');
gridApi.value?.reload();
modalApi.close();
})
.finally(() => {
modalApi.setState({ loading: false, confirmLoading: false });
});
}
});
await formApi.validateAndSubmitForm();
const values = await formApi.getValues();
if (!validateFormValues(values)) {
return;
}
const payload = buildSubmitPayload(values);
modalApi.setState({ loading: true, confirmLoading: true });
const submitApi = isUpdate.value ? updateRegion : createRegion;
submitApi(payload)
.then(() => {
message.success('保存成功');
onSuccessRef.value?.();
modalApi.close();
})
.finally(() => {
modalApi.setState({ loading: false, confirmLoading: false });
});
},
onOpenChange(isOpen: boolean) {
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
if (isOpen) {
const { values, update } = modalApi.getData<Record<string, any>>();
const { values, update, formLevel, onSuccess } = modalApi.getData<
Record<string, any>
>();
isUpdate.value = update;
onSuccessRef.value = onSuccess ?? null;
const levelLabels: Record<number, string> = {
1: '地区',
2: '子地区',
};
modalTitle.value = `${update ? '编辑' : '新增'}${levelLabels[formLevel] ?? '快递费'}`;
formApi.resetForm();
if (values) {
isUpdate.value = update;
formApi.setValues(values);
formApi.setValues({ ...values, formLevel });
}
} else {
onSuccessRef.value = null;
}
},
});
</script>
<template>
<Modal :title="`${isUpdate === true ? '编辑' : '新增'}快递费`" class="w-[30%]">
<Modal :title="modalTitle" class="w-[32%]">
<Form />
</Modal>
</template>

View File

@@ -0,0 +1,87 @@
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Form, FormItem, Select, message } from 'ant-design-vue';
import { updateRegion } from '#/views/system/region/api';
import type { RegionRow } from '../constants';
const moveTarget = ref<RegionRow | null>(null);
const targetId = ref<number | undefined>(undefined);
const level1List = ref<RegionRow[]>([]);
const onSuccess = ref<(() => void) | null>(null);
const options = computed(() =>
level1List.value
.filter((item) => item.id !== moveTarget.value?.pid)
.map((item) => ({
label: item.name,
value: item.id,
})),
);
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
draggable: true,
onCancel() {
modalApi.close();
},
onConfirm: async () => {
if (!moveTarget.value || !targetId.value) {
message.error('请选择目标地区');
return;
}
modalApi.setState({ confirmLoading: true });
try {
await updateRegion({
id: moveTarget.value.id,
pid: targetId.value,
});
message.success('移动成功');
onSuccess.value?.();
modalApi.close();
} finally {
modalApi.setState({ confirmLoading: false });
}
},
onOpenChange(isOpen: boolean) {
if (isOpen) {
const data = modalApi.getData<{
level1List: RegionRow[];
onSuccess?: () => void;
target: RegionRow;
}>();
moveTarget.value = data?.target ?? null;
level1List.value = data?.level1List ?? [];
onSuccess.value = data?.onSuccess ?? null;
targetId.value = moveTarget.value?.pid || undefined;
} else {
moveTarget.value = null;
targetId.value = undefined;
onSuccess.value = null;
}
},
});
</script>
<template>
<Modal title="移动子地区到地区" class="w-[400px]">
<Form layout="vertical">
<FormItem label="目标地区" required>
<Select
v-model:value="targetId"
:options="options"
placeholder="请选择"
show-search
:filter-option="
(input: string, option: any) =>
option?.label?.toLowerCase().includes(input.toLowerCase())
"
/>
</FormItem>
</Form>
</Modal>
</template>

View File

@@ -0,0 +1,91 @@
<script lang="ts" setup>
import { Descriptions, message } from 'ant-design-vue';
import { updateRegion } from '#/views/system/region/api';
import ConfigSectionHeader from '#/views/system/shared/config-section-header.vue';
import InlineFieldEditor from '#/views/system/process/components/inline-field-editor.vue';
import type { RegionRow } from '../constants';
const props = defineProps<{
region: RegionRow | null | undefined;
}>();
const emit = defineEmits<{
updated: [patch: Partial<RegionRow>];
}>();
function formatFee(value: unknown) {
if (value === null || value === undefined || value === '') {
return '点击设置';
}
const num = Number(value);
return Number.isFinite(num) ? `¥${num.toFixed(2)}` : String(value);
}
async function saveField(payload: Record<string, unknown>) {
if (!props.region) {
return;
}
await updateRegion({
id: props.region.id,
...payload,
});
emit('updated', payload as Partial<RegionRow>);
message.success('保存成功');
}
</script>
<template>
<section class="rounded-lg border border-border bg-muted/30 dark:border-gray-700">
<ConfigSectionHeader class="mb-0 px-4 pt-3" title="地区信息" />
<div v-if="region" class="px-4 pb-3 pt-1">
<Descriptions
bordered
:column="3"
size="small"
class="region-info-descriptions"
>
<Descriptions.Item label="级别">地区</Descriptions.Item>
<Descriptions.Item label="名称">
<InlineFieldEditor
:value="region.name"
label="名称"
placeholder="请输入名称"
@save="(val) => saveField({ name: val })"
/>
</Descriptions.Item>
<Descriptions.Item label="地区编码">
{{ region.code || '-' }}
</Descriptions.Item>
<Descriptions.Item label="快递费" :span="2">
<InlineFieldEditor
type="number"
:value="region.express_fee"
label="快递费"
placeholder="请输入价格"
:display-formatter="formatFee"
@save="(val) => saveField({ express_fee: val })"
/>
</Descriptions.Item>
</Descriptions>
</div>
</section>
</template>
<style scoped>
.region-info-descriptions :deep(.ant-descriptions-view) {
border-color: hsl(var(--border));
}
.region-info-descriptions :deep(.ant-descriptions-item-label) {
color: hsl(var(--muted-foreground));
background: hsl(var(--muted) / 0.3);
}
.region-info-descriptions :deep(.ant-descriptions-item-content) {
color: hsl(var(--foreground));
background: hsl(var(--card));
}
</style>

View File

@@ -0,0 +1,150 @@
import { computed, ref } from 'vue';
import { getRegionList } from '#/views/system/region/api';
import type { RegionRow } from '../constants';
import {
readRegionSelection,
writeRegionSelection,
} from '../utils/regionSelectionStorage';
export function parseRegionListItems(items: Record<string, any>[]) {
const level1: RegionRow[] = [];
const level2Map: Record<number, RegionRow[]> = {};
for (const item of items) {
const row: RegionRow = {
id: Number(item.id),
name: String(item.name ?? ''),
pid: Number(item.pid ?? 0),
code: item.code,
level: item.level,
express_fee: item.express_fee,
created_at: item.created_at,
};
if (Number(row.pid) === 0) {
level1.push(row);
continue;
}
const parentId = Number(row.pid);
if (!level2Map[parentId]) {
level2Map[parentId] = [];
}
level2Map[parentId].push(row);
}
level1.sort((a, b) => a.id - b.id);
Object.values(level2Map).forEach((list) => list.sort((a, b) => a.id - b.id));
return { level1, level2Map };
}
function restoreSelection(level1: RegionRow[], savedLevel1Id: number | null) {
let level1Id = savedLevel1Id;
if (!level1Id || !level1.some((item) => item.id === level1Id)) {
level1Id = level1[0]?.id ?? null;
}
return { level1Id };
}
export function useRegionTree() {
const saved = readRegionSelection();
const loading = ref(false);
const keyword = ref('');
const level1List = ref<RegionRow[]>([]);
const level2Map = ref<Record<number, RegionRow[]>>({});
const selectedLevel1Id = ref<number | null>(saved.level1Id);
const filteredLevel1 = computed(() => {
const kw = keyword.value.trim().toLowerCase();
if (!kw) {
return level1List.value;
}
return level1List.value.filter((item) => {
const children = level2Map.value[item.id] ?? [];
if (item.name.toLowerCase().includes(kw)) {
return true;
}
return children.some((child) => child.name.toLowerCase().includes(kw));
});
});
const currentChildren = computed(() => {
if (!selectedLevel1Id.value) {
return [];
}
return level2Map.value[selectedLevel1Id.value] ?? [];
});
const selectedLevel1 = computed(() =>
level1List.value.find((item) => item.id === selectedLevel1Id.value),
);
function persistSelection() {
writeRegionSelection({
level1Id: selectedLevel1Id.value,
});
}
async function loadData() {
loading.value = true;
try {
const res = await getRegionList({
page: 1,
pageSize: 500,
});
const items = res?.items ?? [];
const parsed = parseRegionListItems(items);
level1List.value = parsed.level1;
level2Map.value = parsed.level2Map;
const restored = restoreSelection(
level1List.value,
selectedLevel1Id.value,
);
selectedLevel1Id.value = restored.level1Id;
persistSelection();
} finally {
loading.value = false;
}
}
function selectLevel1(id: number) {
selectedLevel1Id.value = id;
persistSelection();
}
function patchRegion(id: number, patch: Partial<RegionRow>) {
const inLevel1 = level1List.value.find((item) => item.id === id);
if (inLevel1) {
Object.assign(inLevel1, patch);
return;
}
for (const list of Object.values(level2Map.value)) {
const found = list.find((item) => item.id === id);
if (found) {
Object.assign(found, patch);
if (patch.pid !== undefined && Number(patch.pid) !== Number(found.pid)) {
loadData();
}
return;
}
}
}
return {
currentChildren,
filteredLevel1,
keyword,
level1List,
level2Map,
loadData,
loading,
patchRegion,
selectLevel1,
selectedLevel1,
selectedLevel1Id,
};
}

View File

@@ -0,0 +1,47 @@
import type { VxeGridProps } from '#/adapter/vxe-table';
import { SUB_LIST_GRID_HEIGHT } from '#/views/system/shared/sub-list-grid';
export const childGridOptions: VxeGridProps = {
columnConfig: {
useKey: true,
},
rowConfig: {
useKey: true,
keyField: 'id',
},
columns: [
{
field: 'level',
title: '级别',
width: 80,
formatter: () => '子地区',
},
{
field: 'name',
title: '名称',
minWidth: 160,
slots: { default: 'name' },
},
{
field: 'code',
title: '地区编码',
minWidth: 120,
},
{
field: 'express_fee',
title: '快递费',
width: 140,
slots: { default: 'express_fee' },
},
],
height: SUB_LIST_GRID_HEIGHT,
border: true,
showOverflow: true,
pagerConfig: {
enabled: false,
},
toolbarConfig: {
enabled: false,
},
};

View File

@@ -1,50 +1,93 @@
import type { VbenFormProps } from '#/adapter/form';
import {getRegionOptionPidApi} from "#/views/system/region/api";
import { getRegionOptionPidApi } from '#/views/system/region/api';
export const modalFormProps: VbenFormProps = {
wrapperClass: 'grid-cols-12', // 24栅格,
wrapperClass: 'grid-cols-12',
commonConfig: {
formItemClass: 'col-span-12',
// 所有表单项
componentProps: {
class: 'w-full',
},
},
// handleSubmit: onSubmit,
layout: 'horizontal',
schema: [
// {
// fieldName: 'baseinfo',
// component: 'Divider',
// label: '基础信息',
// formItemClass: 'col-span-12',
// componentProps: {},
// hideLabel: true,
// renderComponentContent: () => {
// return {
// default: () => {
// return '基础信息';
// },
// };
// },
// },
{
component: 'VbenInput',
fieldName: 'id',
label: 'ID',
formItemClass: 'col-span-6',
dependencies: {
show: false,
triggerFields: ['id'],
},
},
{
component: 'VbenInput',
fieldName: 'formLevel',
label: '表单层级',
dependencies: {
show: false,
triggerFields: ['formLevel'],
},
},
{
component: 'VbenInput',
fieldName: 'level_label',
label: '级别',
componentProps: {
disabled: true,
},
dependencies: {
show(values) {
return Number(values.formLevel) === 2;
},
trigger(_values, form) {
form.setFieldValue('level_label', '子地区');
},
triggerFields: ['formLevel'],
},
},
{
component: 'VbenInput',
componentProps: {
placeholder: '请输入名称',
},
fieldName: 'name',
label: '名称',
},
{
component: 'ApiSelect',
componentProps: {
placeholder: '请选择父级地区',
api: getRegionOptionPidApi,
filterOption: (input: string, option: any) =>
option?.label.toLowerCase().indexOf(input.toLowerCase()) >= 0,
showSearch: true,
afterFetch: (data: { id: number; name: string }[]) => {
return data.map((item: any) => ({
label: item.name,
value: item.id,
}));
},
},
fieldName: 'pid',
label: '父级地区',
dependencies: {
show(values) {
return Number(values.formLevel) === 2;
},
triggerFields: ['formLevel'],
},
},
{
component: 'InputNumber',
componentProps: {
placeholder: '请输入价格',
class: 'w-full',
min: 0,
},
fieldName: 'express_fee',
label: '价格',
label: '快递费',
},
],
showDefaultActions: false,

View File

@@ -0,0 +1,9 @@
export interface RegionRow {
id: number;
name: string;
pid: number;
code?: string;
level?: number;
express_fee?: number | string;
created_at?: number | string;
}

View File

@@ -1,140 +1,301 @@
<script lang="ts" setup>
import type { VxeGridListeners } from '#/adapter/vxe-table';
import { ref } from 'vue';
import { onMounted, watch } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { Button, Image, message } from 'ant-design-vue';
import { Button, Empty, message } from 'ant-design-vue';
import { PlusOutlined } from '@ant-design/icons-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { TableAction } from '#/components/table-action';
import type { ContextMenuItem } from '#/components/context-menu';
import { showContextMenu } from '#/components/context-menu';
import ConfigSectionHeader from '#/views/system/shared/config-section-header.vue';
import ConfigTreeSidebar from '#/views/system/shared/config-tree-sidebar.vue';
import type { TreeSidebarRow } from '#/views/system/shared/types';
import InlineFieldEditor from '#/views/system/process/components/inline-field-editor.vue';
import { deleteRegion } from './api';
import { deleteRegion, updateRegion } from './api';
import FormModalDemo from './components/modal.vue';
import { formOptions } from './config/search';
import { gridOptions } from './config/table';
import MovePidModal from './components/move-pid-modal.vue';
import RegionInfoBar from './components/region-info-bar.vue';
import { useRegionTree } from './composables/useRegionTree';
import type { RegionRow } from './constants';
import { childGridOptions } from './config/child-table';
const hasTopTableDropDownActions = ref(false);
const gridEvents: VxeGridListeners<any> = {
checkboxChange() {
// eslint-disable-next-line no-use-before-define
const records = gridApi.grid.getCheckboxRecords();
hasTopTableDropDownActions.value = records.length > 0;
},
checkboxAll() {
// eslint-disable-next-line no-use-before-define
const records = gridApi.grid.getCheckboxRecords();
hasTopTableDropDownActions.value = records.length > 0;
},
};
const [Grid, gridApi] = useVbenVxeGrid({
formOptions,
gridOptions,
gridEvents,
});
const {
currentChildren,
filteredLevel1,
keyword,
level1List,
loadData,
loading,
patchRegion,
selectLevel1,
selectedLevel1,
selectedLevel1Id,
} = useRegionTree();
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: FormModalDemo,
});
const showModal = (data = {}, isUpdate = false) => {
const [MoveModal, moveModalApi] = useVbenModal({
connectedComponent: MovePidModal,
});
const [ChildGrid, childGridApi] = useVbenVxeGrid({
gridOptions: {
...childGridOptions,
data: [],
},
});
function syncChildGrid() {
childGridApi.setGridOptions({ data: currentChildren.value });
}
onMounted(async () => {
await loadData();
syncChildGrid();
});
watch(currentChildren, syncChildGrid, { deep: true });
watch(selectedLevel1Id, syncChildGrid);
function refresh() {
return loadData().then(syncChildGrid);
}
function formatFee(value: unknown) {
if (value === null || value === undefined || value === '') {
return '点击设置';
}
const num = Number(value);
return Number.isFinite(num) ? `¥${num.toFixed(2)}` : String(value);
}
function openCreateModal(options: {
formLevel: number;
isUpdate?: boolean;
parentRow?: RegionRow;
values?: Record<string, any>;
}) {
const { formLevel, isUpdate = false, parentRow, values = {} } = options;
let formValues: Record<string, any> = { ...values, formLevel };
if (!isUpdate) {
if (formLevel === 1) {
formValues = { pid: 0, formLevel: 1, express_fee: 0 };
} else if (formLevel === 2 && parentRow) {
formValues = {
pid: parentRow.id,
formLevel: 2,
express_fee: 0,
};
}
}
formModalApi.setData({
// 表单值
values: data,
values: formValues,
update: isUpdate,
gridApi,
formLevel,
onSuccess: refresh,
});
formModalApi.open();
};
}
const deleteApi = (row: any) => {
let ids = [];
if (row) {
ids.push(row);
} else {
ids = gridApi.grid.getCheckboxRecords().map((item) => item.id);
}
deleteRegion({ ids }).then(() => {
message.success('删除成功!');
gridApi.query();
function openMoveModal(row: RegionRow) {
moveModalApi.setData({
target: row,
level1List: level1List.value,
onSuccess: refresh,
});
};
const expandAll = () => {
gridApi.grid?.setAllTreeExpand(true);
};
moveModalApi.open();
}
const collapseAll = () => {
gridApi.grid?.setAllTreeExpand(false);
};
function buildLevel1Menus(row: RegionRow): ContextMenuItem[] {
return [
{
key: 'add-child',
label: '新增子地区',
handler: () => openCreateModal({ formLevel: 2, parentRow: row }),
},
];
}
function buildChildMenus(row: RegionRow): ContextMenuItem[] {
return [
{
key: 'move-pid',
label: '移动父级',
handler: () => openMoveModal(row),
},
{
key: 'delete',
label: '删除',
handler: async () => {
await deleteRegion({ ids: [row.id] });
message.success('删除成功');
await refresh();
},
},
];
}
function onLevel1ContextMenu(e: MouseEvent, row: TreeSidebarRow) {
showContextMenu(e, buildLevel1Menus(row as RegionRow), row);
}
function onChildContextMenu(e: MouseEvent, row: RegionRow) {
showContextMenu(e, buildChildMenus(row), row);
}
async function saveLevel1Name(row: TreeSidebarRow, name: string) {
const region = row as RegionRow;
await updateRegion({ id: region.id, name });
patchRegion(region.id, { name });
message.success('保存成功');
}
async function saveChildField(
row: RegionRow,
field: 'express_fee' | 'name',
value: number | string,
) {
const payload: Record<string, unknown> = { id: row.id };
if (field === 'name') {
payload.name = value;
} else {
payload.express_fee = value;
}
await updateRegion(payload);
patchRegion(
row.id,
field === 'name' ? { name: String(value) } : { express_fee: value },
);
message.success('保存成功');
}
function onSelectLevel1(id: number) {
selectLevel1(id);
syncChildGrid();
}
function openCreateChild() {
if (!selectedLevel1.value) {
return;
}
openCreateModal({ formLevel: 2, parentRow: selectedLevel1.value });
}
</script>
<template>
<Page auto-content-height title="快递费管理">
<Page
auto-content-height
description="右键地区或子地区行可新增/移动;点击虚线下划线字段可编辑"
title="快递费管理"
>
<FormModal />
<Grid>
<template #toolbar-buttons>
<TableAction
:actions="[
{
label: '展开全部',
type: 'primary',
// auth: ['超级菜单', 'sys:user:save'],
onClick: expandAll.bind(null),
},
{
label: '收起全部',
type: 'primary',
// auth: ['超级菜单', 'sys:user:save'],
onClick: collapseAll.bind(null),
},
]"
:drop-down-actions="[
]"
>
<template #more>
<Button style="margin-left: 16px">
批量操作
<Icon icon="ant-design:down-outlined" />
</Button>
</template>
</TableAction>
</template>
<template #logo="{ row }">
<Image :src="row.logo" height="30" width="30" />
</template>
<template #open_business_license="{ row }">
<Image :src="row.open_business_license" height="30" width="30" />
</template>
<template #business_license="{ row }">
<Image :src="row.business_license" height="30" width="30" />
</template>
<template #product_registration_certificate="{ row }">
<Image
:src="row.product_registration_certificate"
height="30"
width="30"
/>
</template>
<template #toolbar-tools></template>
<template #action="{ row }">
<TableAction
:actions="[
{
label: '编辑',
type: 'link',
icon: 'uil:edit',
size: 'small',
// auth: ['region', 'sys:role:detail'],
onClick: showModal.bind(null, row, true),
},
]"
:drop-down-actions="[
]"
/>
</template>
</Grid>
<MoveModal />
<div class="flex h-full min-h-0 gap-4">
<ConfigTreeSidebar
add-label="新增地区"
empty-text="暂无地区"
:filtered-level1="filteredLevel1"
:keyword="keyword"
:loading="loading"
search-placeholder="搜索地区名称"
:selected-level1-id="selectedLevel1Id"
title="地区"
:total-count="level1List.length"
@add-level1="openCreateModal({ formLevel: 1 })"
@contextmenu="onLevel1ContextMenu"
@save-name="saveLevel1Name"
@select="onSelectLevel1"
@update:keyword="(val) => (keyword = val)"
/>
<main
class="min-w-0 flex-1 rounded-lg border border-border bg-card px-4 py-3 dark:border-gray-700"
>
<template v-if="selectedLevel1">
<div class="space-y-4">
<div
class="flex flex-wrap items-center justify-between gap-3 border-b border-border pb-3"
>
<span class="text-sm font-medium text-foreground">{{
selectedLevel1.name
}}</span>
<Button size="small" @click="openCreateChild">
<PlusOutlined />
新增子地区
</Button>
</div>
<RegionInfoBar
:region="selectedLevel1"
@updated="
(patch) => patchRegion(selectedLevel1!.id, patch)
"
/>
<section
class="rounded-lg border border-border dark:border-gray-700"
>
<ConfigSectionHeader
class="mb-3 px-4 pt-3"
:subtitle="`共 ${currentChildren.length} 条`"
title="子地区列表"
>
<template #actions>
<Button size="small" type="primary" @click="openCreateChild">
<PlusOutlined />
新增子地区
</Button>
</template>
</ConfigSectionHeader>
<div
v-if="currentChildren.length"
class="overflow-hidden rounded-b-lg border-t border-border dark:border-gray-700"
>
<ChildGrid @contextmenu.prevent>
<template #name="{ row }">
<span @contextmenu="onChildContextMenu($event, row)">
<InlineFieldEditor
:value="row.name"
@save="(val) => saveChildField(row, 'name', val)"
/>
</span>
</template>
<template #express_fee="{ row }">
<span @contextmenu="onChildContextMenu($event, row)">
<InlineFieldEditor
type="number"
:value="row.express_fee"
:display-formatter="formatFee"
@save="(val) => saveChildField(row, 'express_fee', val)"
/>
</span>
</template>
</ChildGrid>
</div>
<div
v-else
class="flex flex-col items-center justify-center px-4 py-8"
>
<Empty description="暂无子地区" />
<Button class="mt-4" type="primary" @click="openCreateChild">
<PlusOutlined />
新增子地区
</Button>
</div>
</section>
</div>
</template>
<Empty v-else description="请选择左侧地区" />
</main>
</div>
</Page>
</template>

View File

@@ -0,0 +1,32 @@
const STORAGE_KEY = 'region-management-selection';
export interface RegionSelectionState {
level1Id: number | null;
}
export function readRegionSelection(): RegionSelectionState {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) {
return { level1Id: null };
}
const parsed = JSON.parse(raw) as Partial<RegionSelectionState & { childId?: number }>;
return {
level1Id:
parsed.level1Id != null && Number.isFinite(Number(parsed.level1Id))
? Number(parsed.level1Id)
: null,
};
} catch {
return { level1Id: null };
}
}
export function writeRegionSelection(state: RegionSelectionState) {
localStorage.setItem(
STORAGE_KEY,
JSON.stringify({
level1Id: state.level1Id,
}),
);
}

View File

@@ -0,0 +1,20 @@
<script lang="ts" setup>
defineProps<{
subtitle?: string;
title: string;
}>();
</script>
<template>
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
<div class="text-sm font-medium text-foreground">{{ title }}</div>
<div v-if="subtitle" class="mt-0.5 text-xs text-muted-foreground">
{{ subtitle }}
</div>
</div>
<div v-if="$slots.actions" class="flex shrink-0 flex-wrap items-center gap-2">
<slot name="actions" />
</div>
</div>
</template>

View File

@@ -0,0 +1,100 @@
<script lang="ts" setup>
import { Badge, Button, Empty, Input, Spin } from 'ant-design-vue';
import { PlusOutlined, SearchOutlined } from '@ant-design/icons-vue';
import InlineFieldEditor from '#/views/system/process/components/inline-field-editor.vue';
import type { TreeSidebarRow } from './types';
export type { TreeSidebarRow };
withDefaults(
defineProps<{
addLabel: string;
emptyText: string;
filteredLevel1: TreeSidebarRow[];
keyword: string;
loading: boolean;
searchPlaceholder: string;
selectedLevel1Id: number | null;
title: string;
totalCount: number;
}>(),
{
addLabel: '新增',
emptyText: '暂无数据',
searchPlaceholder: '搜索名称',
title: '分类',
},
);
const emit = defineEmits<{
addLevel1: [];
contextmenu: [event: MouseEvent, row: TreeSidebarRow];
saveName: [row: TreeSidebarRow, name: string];
select: [id: number];
'update:keyword': [value: string];
}>();
</script>
<template>
<aside
class="flex w-[240px] shrink-0 flex-col rounded-lg border border-border bg-card p-3"
>
<div class="mb-3 flex items-center justify-between gap-2">
<span class="text-sm font-medium text-foreground">{{ title }}</span>
<Badge
:count="totalCount"
:number-style="{ backgroundColor: 'hsl(var(--primary))' }"
:overflow-count="999"
show-zero
/>
</div>
<Button block class="mb-3" type="primary" @click="emit('addLevel1')">
<PlusOutlined />
{{ addLabel }}
</Button>
<Input
:value="keyword"
allow-clear
class="mb-3"
:placeholder="searchPlaceholder"
@update:value="(val) => emit('update:keyword', val ?? '')"
>
<template #prefix>
<SearchOutlined class="text-muted-foreground" />
</template>
</Input>
<Spin :spinning="loading">
<div
v-if="filteredLevel1.length"
class="flex max-h-[calc(100vh-300px)] flex-col gap-1 overflow-auto"
>
<div
v-for="item in filteredLevel1"
:key="item.id"
class="relative cursor-pointer rounded-md py-2.5 pl-3 pr-2 transition-colors duration-200 hover:bg-muted/50"
:class="{
'bg-primary/10 ring-1 ring-primary/20': selectedLevel1Id === item.id,
}"
@click="emit('select', item.id)"
@contextmenu="emit('contextmenu', $event, item)"
>
<span
v-if="selectedLevel1Id === item.id"
class="absolute bottom-2 left-0 top-2 w-[3px] rounded-full bg-primary"
/>
<InlineFieldEditor
:value="item.name"
@click.stop
@save="(val) => emit('saveName', item, String(val))"
/>
</div>
</div>
<Empty v-else :description="emptyText" />
</Spin>
</aside>
</template>

View File

@@ -0,0 +1 @@
export const SUB_LIST_GRID_HEIGHT = 400;

View File

@@ -0,0 +1,4 @@
export interface TreeSidebarRow {
id: number;
name: string;
}

View File

@@ -42,6 +42,17 @@ const showImportAlert = computed(
() => mode.value === 'report' && importReportId.value > 0,
);
const isRejectedResubmit = computed(() => {
const status = Number(reportInfo.value?.report_status ?? 0);
return Boolean(reportInfo.value?.id) && [3, 4].includes(status);
});
const showPendingSyncHint = computed(
() =>
Boolean(reportInfo.value?.id) &&
Number(reportInfo.value?.report_status) === 1,
);
const [Form, formApi] = useVbenForm(bankCardReportFormProps);
async function loadSelectableCards() {
@@ -95,7 +106,18 @@ const [Modal, modalApi] = useVbenModal({
const values = await formApi.getValues();
modalApi.setState({ loading: true, confirmLoading: true });
try {
if (mode.value === 'report') {
const shouldUpdate =
mode.value === 'update' || isRejectedResubmit.value;
if (shouldUpdate) {
await updateStoreBankCard({
...values,
id: values.id,
use_type: USE_TYPE_TRANSFER,
});
message.success(
isRejectedResubmit.value ? '银行卡报备已重新提交' : '银行卡修改已提交',
);
} else {
await reportStoreBankCard({
...values,
store_id: values.store_id,
@@ -105,13 +127,6 @@ const [Modal, modalApi] = useVbenModal({
message.success(
importReportId.value > 0 ? '银行卡报备已导入' : '银行卡报备已提交',
);
} else {
await updateStoreBankCard({
...values,
id: values.id,
use_type: USE_TYPE_TRANSFER,
});
message.success('银行卡修改已提交');
}
gridApi.value?.reload();
modalApi.close();
@@ -134,6 +149,10 @@ const [Modal, modalApi] = useVbenModal({
reportInfo.value = data?.report ?? null;
importReportId.value = 0;
const rejectedResubmit = [3, 4].includes(
Number(reportInfo.value?.report_status ?? 0),
);
formApi.resetForm();
formApi.setValues({
store_id: storeId.value,
@@ -141,10 +160,16 @@ const [Modal, modalApi] = useVbenModal({
use_type: USE_TYPE_TRANSFER,
use_type_display: '转账卡useType=2',
account_type: reportInfo.value?.account_type ?? '2',
report_status: reportInfo.value?.report_status ?? 0,
...(reportInfo.value ?? {}),
id: reportInfo.value?.id,
attachment: rejectedResubmit ? '' : (reportInfo.value?.attachment ?? ''),
});
loadSelectableCards();
if (reportInfo.value?.id && Number(reportInfo.value?.report_status) === 1) {
refreshStatus({ silent: true });
}
},
});
@@ -153,7 +178,7 @@ const modalTitle = computed(() => {
return storeName.value ? `${prefix} - ${storeName.value}` : prefix;
});
async function refreshStatus() {
async function refreshStatus(options: { silent?: boolean } = {}) {
if (!reportInfo.value?.id) {
return;
}
@@ -161,14 +186,19 @@ async function refreshStatus() {
try {
const res = await queryStoreBankCardStatus({ id: reportInfo.value.id });
reportInfo.value = res;
const rejectedResubmit = [3, 4].includes(Number(res.report_status ?? 0));
formApi.setValues({
...res,
store_id: res.store_id,
use_type: USE_TYPE_TRANSFER,
use_type_display: '转账卡useType=2',
report_status: res.report_status ?? 0,
only_remark: res.can_full_update === false ? 1 : 0,
attachment: rejectedResubmit ? '' : (res.attachment ?? ''),
});
message.success('状态已刷新');
if (!options.silent) {
message.success('状态已刷新');
}
gridApi.value?.reload();
} finally {
modalApi.setState({ loading: false });
@@ -192,6 +222,20 @@ async function refreshStatus() {
show-icon
type="warning"
/>
<Alert
v-if="isRejectedResubmit"
message="审核未通过,请修改信息后重新上传合同附件并提交"
class="mb-4"
show-icon
type="error"
/>
<Alert
v-if="showPendingSyncHint"
message="当前为待审核状态,如审核结果已久未更新,请先点击下方「刷新报备状态」"
class="mb-4"
show-icon
type="warning"
/>
<Alert
v-if="showImportAlert"
message="已从历史报备导入,不会重复提交易票联,请上传本门店合同附件"

View File

@@ -33,6 +33,12 @@ export const bankCardReportFormProps: VbenFormProps = {
label: '仅备注',
dependencies: { show: false, triggerFields: ['only_remark'] },
},
{
component: 'VbenInput',
fieldName: 'report_status',
label: '报备状态',
dependencies: { show: false, triggerFields: ['report_status'] },
},
{
component: 'VbenInput',
fieldName: 'use_type',
@@ -136,8 +142,16 @@ export const bankCardReportFormProps: VbenFormProps = {
label: '合同附件',
rules: 'required',
dependencies: {
show: (values) => !values.only_remark && !values.id,
triggerFields: ['only_remark', 'id'],
show: (values) => {
if (values.only_remark) {
return false;
}
if (!values.id) {
return true;
}
return [3, 4].includes(Number(values.report_status ?? 0));
},
triggerFields: ['only_remark', 'id', 'report_status'],
},
},
{

View File

@@ -121,10 +121,13 @@ const openBankCardReport = async (row: any, update = false) => {
} catch {
report = null;
}
const reportStatus = Number(report?.report_status ?? 0);
const useUpdate =
(update || [3, 4].includes(reportStatus)) && Boolean(report?.id);
bankCardReportModalApi.setData({
storeId: row.id,
storeName: row.name,
mode: update && report?.id ? 'update' : 'report',
mode: useUpdate ? 'update' : 'report',
report,
gridApi,
});

408
pnpm-lock.yaml generated
View File

@@ -650,6 +650,9 @@ importers:
dayjs:
specifier: 'catalog:'
version: 1.11.13
exceljs:
specifier: ^4.4.0
version: 4.4.0
markdown-it:
specifier: ^14.1.0
version: 14.1.0
@@ -3189,6 +3192,12 @@ packages:
resolution: {integrity: sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@fast-csv/format@4.3.5':
resolution: {integrity: sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==}
'@fast-csv/parse@4.3.6':
resolution: {integrity: sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==}
'@floating-ui/core@1.6.8':
resolution: {integrity: sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==}
@@ -3276,8 +3285,8 @@ packages:
resolution: {integrity: sha512-bmsP4L2HqBF6i6uaMqJMcFBONVjKt+siGluRq4Ca4C0q7W2eMaVZr8iCgF9dKbcVXutftkC7D6z2SaSMmLiDyA==}
engines: {node: '>= 16'}
'@intlify/shared@11.4.4':
resolution: {integrity: sha512-QRUCHqda1U6aR14FR0vvXD4+4gj6+fm0AhAozvSuRCw0fCvrmCugWpgiR4xH2NI6s8am6N9p5OhirplsX8ZS3g==}
'@intlify/shared@11.4.6':
resolution: {integrity: sha512-m1p1HHAMLhqSpTRH7VnXdrN0CQ4y+9vunFkpLkbD8soIuBsnQdawZXqMCgvwI2UVF9Ww7sVaw7g9tV2VO7shoA==}
engines: {node: '>= 22'}
'@intlify/shared@12.0.0-alpha.4':
@@ -4001,6 +4010,9 @@ packages:
'@types/node@12.20.55':
resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==}
'@types/node@14.18.63':
resolution: {integrity: sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==}
'@types/node@22.10.2':
resolution: {integrity: sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==}
@@ -4445,10 +4457,22 @@ packages:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
archiver-utils@2.1.0:
resolution: {integrity: sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==}
engines: {node: '>= 6'}
archiver-utils@3.0.4:
resolution: {integrity: sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==}
engines: {node: '>= 10'}
archiver-utils@5.0.2:
resolution: {integrity: sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==}
engines: {node: '>= 14'}
archiver@5.3.2:
resolution: {integrity: sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==}
engines: {node: '>= 10'}
archiver@7.0.1:
resolution: {integrity: sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==}
engines: {node: '>= 14'}
@@ -4586,10 +4610,17 @@ packages:
resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==}
engines: {node: '>=4'}
big-integer@1.6.52:
resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==}
engines: {node: '>=0.6'}
binary-extensions@2.3.0:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
engines: {node: '>=8'}
binary@0.3.0:
resolution: {integrity: sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==}
bindings@1.5.0:
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
@@ -4599,6 +4630,12 @@ packages:
birpc@0.2.19:
resolution: {integrity: sha512-5WeXXAvTmitV1RqJFppT5QtUiz2p1mRSYU000Jkft5ZUCLJIk4uQriYNO50HknxKwM6jd8utNc66K1qGIwwWBQ==}
bl@4.1.0:
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
bluebird@3.4.7:
resolution: {integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==}
boolbase@1.0.0:
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
@@ -4621,6 +4658,9 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
buffer-crc32@0.2.13:
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
buffer-crc32@1.0.0:
resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==}
engines: {node: '>=8.0.0'}
@@ -4628,9 +4668,20 @@ packages:
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
buffer-indexof-polyfill@1.0.2:
resolution: {integrity: sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==}
engines: {node: '>=0.10'}
buffer@5.7.1:
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
buffer@6.0.3:
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
buffers@0.1.1:
resolution: {integrity: sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==}
engines: {node: '>=0.2.0'}
builtin-modules@3.3.0:
resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==}
engines: {node: '>=6'}
@@ -4707,6 +4758,9 @@ packages:
resolution: {integrity: sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==}
engines: {node: '>=12'}
chainsaw@0.1.0:
resolution: {integrity: sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==}
chalk-template@1.1.0:
resolution: {integrity: sha512-T2VJbcDuZQ0Tb2EWwSotMPJjgpy1/tGee1BTpUNsGZ/qgNjV2t7Mvu+d4600U564nbLesN1x2dPL+xii174Ekg==}
engines: {node: '>=14.16'}
@@ -4912,6 +4966,10 @@ packages:
compatx@0.1.8:
resolution: {integrity: sha512-jcbsEAR81Bt5s1qOFymBufmCbXCXbk0Ql+K5ouj6gCyx2yHlu6AgmGIi9HxfKixpUDO5bCFJUHQ5uM6ecbTebw==}
compress-commons@4.1.2:
resolution: {integrity: sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==}
engines: {node: '>= 10'}
compress-commons@6.0.2:
resolution: {integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==}
engines: {node: '>= 14'}
@@ -5007,6 +5065,10 @@ packages:
engines: {node: '>=0.8'}
hasBin: true
crc32-stream@4.0.3:
resolution: {integrity: sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==}
engines: {node: '>= 10'}
crc32-stream@6.0.0:
resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==}
engines: {node: '>= 14'}
@@ -5418,6 +5480,9 @@ packages:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
duplexer2@0.1.4:
resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==}
duplexer@0.1.2:
resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
@@ -5466,6 +5531,9 @@ packages:
encoding@0.1.13:
resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==}
end-of-stream@1.4.5:
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
enhanced-resolve@5.18.0:
resolution: {integrity: sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==}
engines: {node: '>=10.13.0'}
@@ -5802,6 +5870,10 @@ packages:
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
engines: {node: '>=0.8.x'}
exceljs@4.4.0:
resolution: {integrity: sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==}
engines: {node: '>=8.3.0'}
execa@8.0.1:
resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==}
engines: {node: '>=16.17'}
@@ -5828,6 +5900,10 @@ packages:
resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==}
engines: {node: '>=4'}
fast-csv@4.3.6:
resolution: {integrity: sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==}
engines: {node: '>=10.0.0'}
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
@@ -5966,6 +6042,9 @@ packages:
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
engines: {node: '>= 0.6'}
fs-constants@1.0.0:
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
fs-extra@10.1.0:
resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==}
engines: {node: '>=12'}
@@ -6003,6 +6082,11 @@ packages:
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
fstream@1.0.12:
resolution: {integrity: sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==}
engines: {node: '>=0.6'}
deprecated: This package is no longer supported.
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
@@ -6324,6 +6408,9 @@ packages:
engines: {node: '>=0.10.0'}
hasBin: true
immediate@3.0.6:
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
immutable@4.3.7:
resolution: {integrity: sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==}
@@ -6761,6 +6848,9 @@ packages:
resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==}
engines: {node: '>=0.10.0'}
jszip@3.10.1:
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
@@ -6806,6 +6896,9 @@ packages:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
lie@3.3.0:
resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
lilconfig@3.1.3:
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
engines: {node: '>=14'}
@@ -6821,6 +6914,9 @@ packages:
engines: {node: '>=18.12.0'}
hasBin: true
listenercount@1.0.1:
resolution: {integrity: sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==}
listhen@1.9.0:
resolution: {integrity: sha512-I8oW2+QL5KJo8zXNWX046M134WchxsXC7SawLPvRQpogCbkyQIaFxPE89A2HiwR7vAK2Dm2ERBAmyjTYGYEpBg==}
hasBin: true
@@ -6863,20 +6959,44 @@ packages:
lodash.defaults@4.2.0:
resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==}
lodash.difference@4.5.0:
resolution: {integrity: sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==}
lodash.escaperegexp@4.1.2:
resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==}
lodash.flatten@4.4.0:
resolution: {integrity: sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==}
lodash.get@4.4.2:
resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==}
deprecated: This package is deprecated. Use the optional chaining (?.) operator instead.
lodash.groupby@4.6.0:
resolution: {integrity: sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==}
lodash.isarguments@3.1.0:
resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==}
lodash.isboolean@3.0.3:
resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
lodash.isequal@4.5.0:
resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==}
deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead.
lodash.isfunction@3.0.9:
resolution: {integrity: sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==}
lodash.isnil@4.0.0:
resolution: {integrity: sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==}
lodash.isplainobject@4.0.6:
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
lodash.isundefined@3.0.1:
resolution: {integrity: sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==}
lodash.kebabcase@4.1.1:
resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==}
@@ -6901,6 +7021,9 @@ packages:
lodash.truncate@4.4.2:
resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==}
lodash.union@4.6.0:
resolution: {integrity: sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==}
lodash.uniq@4.5.0:
resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==}
@@ -7146,6 +7269,10 @@ packages:
mitt@3.0.1:
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
mkdirp@0.5.6:
resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
hasBin: true
mkdirp@1.0.4:
resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}
engines: {node: '>=10'}
@@ -7447,6 +7574,9 @@ packages:
package-manager-detector@0.2.7:
resolution: {integrity: sha512-g4+387DXDKlZzHkP+9FLt8yKj8+/3tOkPv7DVTJGGRm00RkEWgqbFstX1mXJ4M0VDYhUqsTOiISqNOJnhAu3PQ==}
pako@1.0.11:
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
param-case@3.0.4:
resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==}
@@ -8250,6 +8380,10 @@ packages:
readable-stream@2.3.8:
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
readable-stream@3.6.2:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'}
readable-stream@4.6.0:
resolution: {integrity: sha512-cbAdYt0VcnpN2Bekq7PU+k363ZRsPwJoEEJOEtSJQlJXzwaxt3FIo/uL+KeDSGIjJqtkwyge4KQgD2S2kd+CQw==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -8396,6 +8530,11 @@ packages:
rfdc@1.4.1:
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
rimraf@2.7.1:
resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==}
deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true
rimraf@3.0.2:
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
deprecated: Rimraf versions prior to v4 are no longer supported
@@ -8477,6 +8616,10 @@ packages:
sax@1.4.1:
resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==}
saxes@5.0.1:
resolution: {integrity: sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==}
engines: {node: '>=10'}
scroll-into-view-if-needed@2.2.31:
resolution: {integrity: sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==}
@@ -8533,6 +8676,9 @@ packages:
resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==}
engines: {node: '>= 0.4'}
setimmediate@1.0.5:
resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
setprototypeof@1.2.0:
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
@@ -8952,6 +9098,10 @@ packages:
resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==}
engines: {node: '>=6'}
tar-stream@2.2.0:
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
engines: {node: '>=6'}
tar-stream@3.1.7:
resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==}
@@ -9033,6 +9183,10 @@ packages:
resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==}
engines: {node: '>=0.6.0'}
tmp@0.2.7:
resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==}
engines: {node: '>=14.14'}
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
@@ -9051,6 +9205,9 @@ packages:
tr46@1.0.1:
resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==}
traverse@0.3.9:
resolution: {integrity: sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==}
ts-api-utils@1.4.3:
resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==}
engines: {node: '>=16'}
@@ -9313,6 +9470,9 @@ packages:
unwasm@0.3.9:
resolution: {integrity: sha512-LDxTx/2DkFURUd+BU1vUsF/moj0JsoTvl+2tcg2AUOiEzVturhGGx17/IMgGvKUYdZwr33EJHtChCJuhu9Ouvg==}
unzipper@0.10.14:
resolution: {integrity: sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==}
upath@1.2.0:
resolution: {integrity: sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==}
engines: {node: '>=4'}
@@ -9345,6 +9505,10 @@ packages:
utrie@1.0.2:
resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==}
uuid@8.3.2:
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
hasBin: true
validate-npm-package-license@3.0.4:
resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
@@ -9759,6 +9923,9 @@ packages:
resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==}
engines: {node: '>=12'}
xmlchars@2.2.0:
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
xss@1.0.15:
resolution: {integrity: sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg==}
engines: {node: '>= 0.10.0'}
@@ -9833,6 +10000,10 @@ packages:
resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==}
engines: {node: '>=18'}
zip-stream@4.1.1:
resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==}
engines: {node: '>= 10'}
zip-stream@6.0.1:
resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==}
engines: {node: '>= 14'}
@@ -11720,6 +11891,25 @@ snapshots:
dependencies:
levn: 0.4.1
'@fast-csv/format@4.3.5':
dependencies:
'@types/node': 14.18.63
lodash.escaperegexp: 4.1.2
lodash.isboolean: 3.0.3
lodash.isequal: 4.5.0
lodash.isfunction: 3.0.9
lodash.isnil: 4.0.0
'@fast-csv/parse@4.3.6':
dependencies:
'@types/node': 14.18.63
lodash.escaperegexp: 4.1.2
lodash.groupby: 4.6.0
lodash.isfunction: 3.0.9
lodash.isnil: 4.0.0
lodash.isundefined: 3.0.1
lodash.uniq: 4.5.0
'@floating-ui/core@1.6.8':
dependencies:
'@floating-ui/utils': 0.2.8
@@ -11812,7 +12002,7 @@ snapshots:
'@intlify/shared@10.0.5': {}
'@intlify/shared@11.4.4': {}
'@intlify/shared@11.4.6': {}
'@intlify/shared@12.0.0-alpha.4': {}
@@ -11820,8 +12010,8 @@ snapshots:
dependencies:
'@eslint-community/eslint-utils': 4.4.1(eslint@9.17.0(jiti@2.4.2))
'@intlify/bundle-utils': 10.0.0(vue-i18n@10.0.5(vue@3.5.13(typescript@5.7.2)))
'@intlify/shared': 11.4.4
'@intlify/vue-i18n-extensions': 7.0.0(@intlify/shared@11.4.4)(@vue/compiler-dom@3.5.13)(vue-i18n@10.0.5(vue@3.5.13(typescript@5.7.2)))(vue@3.5.13(typescript@5.7.2))
'@intlify/shared': 11.4.6
'@intlify/vue-i18n-extensions': 7.0.0(@intlify/shared@11.4.6)(@vue/compiler-dom@3.5.13)(vue-i18n@10.0.5(vue@3.5.13(typescript@5.7.2)))(vue@3.5.13(typescript@5.7.2))
'@rollup/pluginutils': 5.1.4(rollup@4.28.1)
'@typescript-eslint/scope-manager': 8.18.1
'@typescript-eslint/typescript-estree': 8.18.1(typescript@5.7.2)
@@ -11843,11 +12033,11 @@ snapshots:
- supports-color
- typescript
'@intlify/vue-i18n-extensions@7.0.0(@intlify/shared@11.4.4)(@vue/compiler-dom@3.5.13)(vue-i18n@10.0.5(vue@3.5.13(typescript@5.7.2)))(vue@3.5.13(typescript@5.7.2))':
'@intlify/vue-i18n-extensions@7.0.0(@intlify/shared@11.4.6)(@vue/compiler-dom@3.5.13)(vue-i18n@10.0.5(vue@3.5.13(typescript@5.7.2)))(vue@3.5.13(typescript@5.7.2))':
dependencies:
'@babel/parser': 7.26.3
optionalDependencies:
'@intlify/shared': 11.4.4
'@intlify/shared': 11.4.6
'@vue/compiler-dom': 3.5.13
vue: 3.5.13(typescript@5.7.2)
vue-i18n: 10.0.5(vue@3.5.13(typescript@5.7.2))
@@ -12640,6 +12830,8 @@ snapshots:
'@types/node@12.20.55': {}
'@types/node@14.18.63': {}
'@types/node@22.10.2':
dependencies:
undici-types: 6.20.0
@@ -13251,6 +13443,32 @@ snapshots:
normalize-path: 3.0.0
picomatch: 2.3.1
archiver-utils@2.1.0:
dependencies:
glob: 7.2.3
graceful-fs: 4.2.11
lazystream: 1.0.1
lodash.defaults: 4.2.0
lodash.difference: 4.5.0
lodash.flatten: 4.4.0
lodash.isplainobject: 4.0.6
lodash.union: 4.6.0
normalize-path: 3.0.0
readable-stream: 2.3.8
archiver-utils@3.0.4:
dependencies:
glob: 7.2.3
graceful-fs: 4.2.11
lazystream: 1.0.1
lodash.defaults: 4.2.0
lodash.difference: 4.5.0
lodash.flatten: 4.4.0
lodash.isplainobject: 4.0.6
lodash.union: 4.6.0
normalize-path: 3.0.0
readable-stream: 3.6.2
archiver-utils@5.0.2:
dependencies:
glob: 10.4.5
@@ -13261,6 +13479,16 @@ snapshots:
normalize-path: 3.0.0
readable-stream: 4.6.0
archiver@5.3.2:
dependencies:
archiver-utils: 2.1.0
async: 3.2.6
buffer-crc32: 0.2.13
readable-stream: 3.6.2
readdir-glob: 1.1.3
tar-stream: 2.2.0
zip-stream: 4.1.1
archiver@7.0.1:
dependencies:
archiver-utils: 5.0.2
@@ -13408,8 +13636,15 @@ snapshots:
dependencies:
is-windows: 1.0.2
big-integer@1.6.52: {}
binary-extensions@2.3.0: {}
binary@0.3.0:
dependencies:
buffers: 0.1.1
chainsaw: 0.1.0
bindings@1.5.0:
dependencies:
file-uri-to-path: 1.0.0
@@ -13418,6 +13653,14 @@ snapshots:
birpc@0.2.19: {}
bl@4.1.0:
dependencies:
buffer: 5.7.1
inherits: 2.0.4
readable-stream: 3.6.2
bluebird@3.4.7: {}
boolbase@1.0.0: {}
boxen@8.0.1:
@@ -13451,15 +13694,26 @@ snapshots:
node-releases: 2.0.19
update-browserslist-db: 1.1.1(browserslist@4.24.3)
buffer-crc32@0.2.13: {}
buffer-crc32@1.0.0: {}
buffer-from@1.1.2: {}
buffer-indexof-polyfill@1.0.2: {}
buffer@5.7.1:
dependencies:
base64-js: 1.5.1
ieee754: 1.2.1
buffer@6.0.3:
dependencies:
base64-js: 1.5.1
ieee754: 1.2.1
buffers@0.1.1: {}
builtin-modules@3.3.0: {}
bundle-name@4.1.0:
@@ -13564,6 +13818,10 @@ snapshots:
loupe: 3.1.2
pathval: 2.0.0
chainsaw@0.1.0:
dependencies:
traverse: 0.3.9
chalk-template@1.1.0:
dependencies:
chalk: 5.4.0
@@ -13777,6 +14035,13 @@ snapshots:
compatx@0.1.8: {}
compress-commons@4.1.2:
dependencies:
buffer-crc32: 0.2.13
crc32-stream: 4.0.3
normalize-path: 3.0.0
readable-stream: 3.6.2
compress-commons@6.0.2:
dependencies:
crc-32: 1.2.2
@@ -13873,6 +14138,11 @@ snapshots:
crc-32@1.2.2: {}
crc32-stream@4.0.3:
dependencies:
crc-32: 1.2.2
readable-stream: 3.6.2
crc32-stream@6.0.0:
dependencies:
crc-32: 1.2.2
@@ -14323,6 +14593,10 @@ snapshots:
es-errors: 1.3.0
gopd: 1.2.0
duplexer2@0.1.4:
dependencies:
readable-stream: 2.3.8
duplexer@0.1.2: {}
eastasianwidth@0.2.0: {}
@@ -14367,6 +14641,10 @@ snapshots:
iconv-lite: 0.6.3
optional: true
end-of-stream@1.4.5:
dependencies:
once: 1.4.0
enhanced-resolve@5.18.0:
dependencies:
graceful-fs: 4.2.11
@@ -14820,6 +15098,18 @@ snapshots:
events@3.3.0: {}
exceljs@4.4.0:
dependencies:
archiver: 5.3.2
dayjs: 1.11.13
fast-csv: 4.3.6
jszip: 3.10.1
readable-stream: 3.6.2
saxes: 5.0.1
tmp: 0.2.7
unzipper: 0.10.14
uuid: 8.3.2
execa@8.0.1:
dependencies:
cross-spawn: 7.0.6
@@ -14863,6 +15153,11 @@ snapshots:
iconv-lite: 0.4.24
tmp: 0.0.33
fast-csv@4.3.6:
dependencies:
'@fast-csv/format': 4.3.5
'@fast-csv/parse': 4.3.6
fast-deep-equal@3.1.3: {}
fast-diff@1.1.2: {}
@@ -14988,6 +15283,8 @@ snapshots:
fresh@0.5.2: {}
fs-constants@1.0.0: {}
fs-extra@10.1.0:
dependencies:
graceful-fs: 4.2.11
@@ -15031,6 +15328,13 @@ snapshots:
fsevents@2.3.3:
optional: true
fstream@1.0.12:
dependencies:
graceful-fs: 4.2.11
inherits: 2.0.4
mkdirp: 0.5.6
rimraf: 2.7.1
function-bind@1.1.2: {}
function.prototype.name@1.1.8:
@@ -15398,6 +15702,8 @@ snapshots:
image-size@0.5.5:
optional: true
immediate@3.0.6: {}
immutable@4.3.7: {}
import-fresh@3.3.0:
@@ -15763,6 +16069,13 @@ snapshots:
jsonpointer@5.0.1: {}
jszip@3.10.1:
dependencies:
lie: 3.3.0
pako: 1.0.11
readable-stream: 2.3.8
setimmediate: 1.0.5
keyv@4.5.4:
dependencies:
json-buffer: 3.0.1
@@ -15809,6 +16122,10 @@ snapshots:
prelude-ls: 1.2.1
type-check: 0.4.0
lie@3.3.0:
dependencies:
immediate: 3.0.6
lilconfig@3.1.3: {}
lines-and-columns@1.2.4: {}
@@ -15832,6 +16149,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
listenercount@1.0.1: {}
listhen@1.9.0:
dependencies:
'@parcel/watcher': 2.5.0
@@ -15891,14 +16210,30 @@ snapshots:
lodash.defaults@4.2.0: {}
lodash.difference@4.5.0: {}
lodash.escaperegexp@4.1.2: {}
lodash.flatten@4.4.0: {}
lodash.get@4.4.2: {}
lodash.groupby@4.6.0: {}
lodash.isarguments@3.1.0: {}
lodash.isboolean@3.0.3: {}
lodash.isequal@4.5.0: {}
lodash.isfunction@3.0.9: {}
lodash.isnil@4.0.0: {}
lodash.isplainobject@4.0.6: {}
lodash.isundefined@3.0.1: {}
lodash.kebabcase@4.1.1: {}
lodash.memoize@4.1.2: {}
@@ -15915,6 +16250,8 @@ snapshots:
lodash.truncate@4.4.2: {}
lodash.union@4.6.0: {}
lodash.uniq@4.5.0: {}
lodash.upperfirst@4.3.1: {}
@@ -16165,6 +16502,10 @@ snapshots:
mitt@3.0.1: {}
mkdirp@0.5.6:
dependencies:
minimist: 1.2.8
mkdirp@1.0.4: {}
mkdirp@3.0.1: {}
@@ -16559,6 +16900,8 @@ snapshots:
package-manager-detector@0.2.7: {}
pako@1.0.11: {}
param-case@3.0.4:
dependencies:
dot-case: 3.0.4
@@ -17331,6 +17674,12 @@ snapshots:
string_decoder: 1.1.1
util-deprecate: 1.0.2
readable-stream@3.6.2:
dependencies:
inherits: 2.0.4
string_decoder: 1.3.0
util-deprecate: 1.0.2
readable-stream@4.6.0:
dependencies:
abort-controller: 3.0.0
@@ -17469,6 +17818,10 @@ snapshots:
rfdc@1.4.1: {}
rimraf@2.7.1:
dependencies:
glob: 7.2.3
rimraf@3.0.2:
dependencies:
glob: 7.2.3
@@ -17571,6 +17924,10 @@ snapshots:
sax@1.4.1:
optional: true
saxes@5.0.1:
dependencies:
xmlchars: 2.2.0
scroll-into-view-if-needed@2.2.31:
dependencies:
compute-scroll-into-view: 1.0.20
@@ -17648,6 +18005,8 @@ snapshots:
functions-have-names: 1.2.3
has-property-descriptors: 1.0.2
setimmediate@1.0.5: {}
setprototypeof@1.2.0: {}
shallow-equal@1.2.1: {}
@@ -18146,6 +18505,14 @@ snapshots:
tapable@2.2.1: {}
tar-stream@2.2.0:
dependencies:
bl: 4.1.0
end-of-stream: 1.4.5
fs-constants: 1.0.0
inherits: 2.0.4
readable-stream: 3.6.2
tar-stream@3.1.7:
dependencies:
b4a: 1.6.7
@@ -18231,6 +18598,8 @@ snapshots:
dependencies:
os-tmpdir: 1.0.2
tmp@0.2.7: {}
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
@@ -18245,6 +18614,8 @@ snapshots:
dependencies:
punycode: 2.3.1
traverse@0.3.9: {}
ts-api-utils@1.4.3(typescript@5.7.2):
dependencies:
typescript: 5.7.2
@@ -18505,6 +18876,19 @@ snapshots:
pkg-types: 1.2.1
unplugin: 1.16.0
unzipper@0.10.14:
dependencies:
big-integer: 1.6.52
binary: 0.3.0
bluebird: 3.4.7
buffer-indexof-polyfill: 1.0.2
duplexer2: 0.1.4
fstream: 1.0.12
graceful-fs: 4.2.11
listenercount: 1.0.1
readable-stream: 2.3.8
setimmediate: 1.0.5
upath@1.2.0: {}
update-browserslist-db@1.1.1(browserslist@4.24.3):
@@ -18542,6 +18926,8 @@ snapshots:
dependencies:
base64-arraybuffer: 1.0.2
uuid@8.3.2: {}
validate-npm-package-license@3.0.4:
dependencies:
spdx-correct: 3.2.0
@@ -19087,6 +19473,8 @@ snapshots:
xml-name-validator@4.0.0: {}
xmlchars@2.2.0: {}
xss@1.0.15:
dependencies:
commander: 2.20.3
@@ -19163,6 +19551,12 @@ snapshots:
yoctocolors@2.1.1: {}
zip-stream@4.1.1:
dependencies:
archiver-utils: 3.0.4
compress-commons: 4.1.2
readable-stream: 3.6.2
zip-stream@6.0.1:
dependencies:
archiver-utils: 5.0.2