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
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:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -49,3 +49,4 @@ vite.config.ts.*
|
||||
*.sln
|
||||
*.sw?
|
||||
.history
|
||||
/.cursor/
|
||||
|
||||
@@ -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:",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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' ? '商品明细维度' : '订单维度';
|
||||
}
|
||||
@@ -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: '展开全部',
|
||||
|
||||
@@ -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(' → ');
|
||||
}
|
||||
@@ -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|value;right 省略时右侧合并留空 */
|
||||
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`;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -52,7 +52,7 @@ function displayText() {
|
||||
|
||||
|
||||
|
||||
return '-';
|
||||
const emit = defineEmits<{
|
||||
|
||||
save: [value: number | string];
|
||||
|
||||
@@ -63,6 +63,10 @@ function displayText() {
|
||||
const open = ref(false);
|
||||
|
||||
const draftText = ref('');
|
||||
|
||||
const draftNumber = ref<null | number>(null);
|
||||
|
||||
const saving = ref(false);
|
||||
|
||||
|
||||
|
||||
@@ -93,14 +97,14 @@ function selectTag(value: number | string) {
|
||||
|
||||
|
||||
function displayText() {
|
||||
<div class="inline-editor-popover">
|
||||
<div v-if="label" class="inline-editor-popover__label">{{ label }}</div>
|
||||
|
||||
if (props.displayFormatter) {
|
||||
|
||||
return props.displayFormatter(props.value);
|
||||
|
||||
}
|
||||
|
||||
class="cursor-pointer"
|
||||
if (props.value === null || props.value === undefined || props.value === '') {
|
||||
|
||||
return '点击设置';
|
||||
|
||||
@@ -147,29 +151,21 @@ function selectTag(value: number | string) {
|
||||
}
|
||||
|
||||
emit('save', draftNumber.value);
|
||||
<Button
|
||||
class="h-auto p-0 text-left"
|
||||
|
||||
} else {
|
||||
|
||||
const text = draftText.value.trim();
|
||||
|
||||
if (!text) {
|
||||
|
||||
size="small"
|
||||
type="link"
|
||||
return;
|
||||
|
||||
}
|
||||
</Button>
|
||||
|
||||
emit('save', text);
|
||||
|
||||
}
|
||||
|
||||
.inline-editor-popover {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.inline-editor-popover__label {
|
||||
margin-bottom: 8px;
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
open.value = false;
|
||||
|
||||
} finally {
|
||||
|
||||
@@ -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>
|
||||
</div>
|
||||
<div class="method-info-bar__item">
|
||||
<span class="method-info-bar__label">名称</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 })"
|
||||
|
||||
/>
|
||||
</div>
|
||||
<div class="method-info-bar__item">
|
||||
<span class="method-info-bar__label">计算方式</span>
|
||||
|
||||
</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 ?? '-')
|
||||
|
||||
(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>
|
||||
|
||||
</Descriptions.Item>
|
||||
|
||||
<Descriptions.Item label="计算价格">
|
||||
|
||||
<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>
|
||||
|
||||
</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 v-else class="flex justify-center px-4 py-6">
|
||||
|
||||
<Empty description="请选择上方煎法 Tab" />
|
||||
|
||||
</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>
|
||||
|
||||
|
||||
@@ -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>) {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,64 +244,95 @@ 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="搜索名称"
|
||||
<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)"
|
||||
/>
|
||||
<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>
|
||||
|
||||
<main class="process-main">
|
||||
<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 })"
|
||||
<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"
|
||||
>
|
||||
<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
|
||||
:class="
|
||||
selectedMethod
|
||||
? 'text-foreground'
|
||||
: 'text-muted-foreground'
|
||||
"
|
||||
>
|
||||
{{ 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 ?? '')"
|
||||
type="card"
|
||||
class="process-method-tabs select-none"
|
||||
type="line"
|
||||
@change="onTabChange"
|
||||
>
|
||||
<Tabs.TabPane
|
||||
@@ -309,23 +341,60 @@ function onSelectLevel1(id: number) {
|
||||
>
|
||||
<template #tab>
|
||||
<span
|
||||
@contextmenu.prevent="onMethodTabContextMenu($event, method)"
|
||||
class="cursor-pointer"
|
||||
@contextmenu.prevent="
|
||||
onMethodTabContextMenu($event, method)
|
||||
"
|
||||
>
|
||||
{{ method.name }}
|
||||
</span>
|
||||
</template>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
<Empty
|
||||
<div
|
||||
v-else
|
||||
description="暂无煎法,请右键制剂或点击新增"
|
||||
/>
|
||||
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)"
|
||||
@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)">
|
||||
@@ -344,13 +413,29 @@ function onSelectLevel1(id: number) {
|
||||
(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>
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -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();
|
||||
if (!validateFormValues(values)) {
|
||||
return;
|
||||
}
|
||||
const payload = buildSubmitPayload(values);
|
||||
modalApi.setState({ loading: true, confirmLoading: true });
|
||||
const submitApi = isUpdate.value ? updateRegion : createRegion;
|
||||
submitApi(values)
|
||||
submitApi(payload)
|
||||
.then(() => {
|
||||
message.success('保存成功');
|
||||
gridApi.value?.reload();
|
||||
onSuccessRef.value?.();
|
||||
modalApi.close();
|
||||
})
|
||||
.finally(() => {
|
||||
modalApi.setState({ loading: false, confirmLoading: false });
|
||||
});
|
||||
}
|
||||
});
|
||||
await formApi.validateAndSubmitForm();
|
||||
},
|
||||
onOpenChange(isOpen: boolean) {
|
||||
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
|
||||
if (isOpen) {
|
||||
const { values, update } = modalApi.getData<Record<string, any>>();
|
||||
if (values) {
|
||||
const { values, update, formLevel, onSuccess } = modalApi.getData<
|
||||
Record<string, any>
|
||||
>();
|
||||
isUpdate.value = update;
|
||||
formApi.setValues(values);
|
||||
onSuccessRef.value = onSuccess ?? null;
|
||||
const levelLabels: Record<number, string> = {
|
||||
1: '地区',
|
||||
2: '子地区',
|
||||
};
|
||||
modalTitle.value = `${update ? '编辑' : '新增'}${levelLabels[formLevel] ?? '快递费'}`;
|
||||
formApi.resetForm();
|
||||
if (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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
47
apps/web-antd/src/views/system/region/config/child-table.ts
Normal file
47
apps/web-antd/src/views/system/region/config/child-table.ts
Normal 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,
|
||||
},
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
9
apps/web-antd/src/views/system/region/constants.ts
Normal file
9
apps/web-antd/src/views/system/region/constants.ts
Normal 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;
|
||||
}
|
||||
@@ -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="快递费管理">
|
||||
<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="[
|
||||
]"
|
||||
<Page
|
||||
auto-content-height
|
||||
description="右键地区或子地区行可新增/移动;点击虚线下划线字段可编辑"
|
||||
title="快递费管理"
|
||||
>
|
||||
<template #more>
|
||||
<Button style="margin-left: 16px">
|
||||
批量操作
|
||||
<Icon icon="ant-design:down-outlined" />
|
||||
<FormModal />
|
||||
<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>
|
||||
</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"
|
||||
</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 #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 #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>
|
||||
</Grid>
|
||||
</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>
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
100
apps/web-antd/src/views/system/shared/config-tree-sidebar.vue
Normal file
100
apps/web-antd/src/views/system/shared/config-tree-sidebar.vue
Normal 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>
|
||||
1
apps/web-antd/src/views/system/shared/sub-list-grid.ts
Normal file
1
apps/web-antd/src/views/system/shared/sub-list-grid.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const SUB_LIST_GRID_HEIGHT = 400;
|
||||
4
apps/web-antd/src/views/system/shared/types.ts
Normal file
4
apps/web-antd/src/views/system/shared/types.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface TreeSidebarRow {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
@@ -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 ?? ''),
|
||||
});
|
||||
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="已从历史报备导入,不会重复提交易票联,请上传本门店合同附件"
|
||||
|
||||
@@ -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'],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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
408
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user