fix: 修复商品模块typecheck与lint
All checks were successful
Build and Deploy TenantUI / build-and-deploy (push) Successful in 51s

This commit is contained in:
2026-02-26 15:06:14 +08:00
parent 58c37d9b00
commit 5abd5b2abe
20 changed files with 120 additions and 62 deletions

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import type { UploadProps } from 'ant-design-vue';
import { Button, Select, Space, Upload } from 'ant-design-vue';
interface Props {
@@ -69,10 +70,7 @@ function setExportCategoryIds(value: unknown) {
已选择{{ props.selectedFileName }}
</div>
<Space class="pbt-upload-actions">
<Button
:loading="props.submitting"
@click="emit('downloadTemplate')"
>
<Button :loading="props.submitting" @click="emit('downloadTemplate')">
下载导入模板
</Button>
<Button
@@ -133,4 +131,3 @@ function setExportCategoryIds(value: unknown) {
</footer>
</section>
</template>

View File

@@ -84,4 +84,3 @@ function setTargetCategory(value: unknown) {
</footer>
</section>
</template>

View File

@@ -2,6 +2,7 @@
import type { BatchPricePreviewRow, BatchScopeType } from '../types';
import { computed } from 'vue';
import { Button, InputNumber, Select, Table } from 'ant-design-vue';
interface Props {
@@ -223,4 +224,3 @@ function setAmount(value: null | number | string) {
</footer>
</section>
</template>

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import type { BatchScopeType } from '../types';
import { Button, Select } from 'ant-design-vue';
interface Props {
@@ -135,4 +136,3 @@ function setScopeProductIds(value: unknown) {
</footer>
</section>
</template>

View File

@@ -129,4 +129,3 @@ function setSyncStatus(value: boolean | string | undefined) {
</footer>
</section>
</template>

View File

@@ -2,6 +2,7 @@
import type { BatchToolCard, BatchToolKey } from '../types';
import { IconifyIcon } from '@vben/icons';
import { Button } from 'ant-design-vue';
interface Props {
@@ -41,4 +42,3 @@ const emit = defineEmits<Emits>();
</article>
</div>
</template>

View File

@@ -40,7 +40,10 @@ export const BATCH_TOOL_CARDS: BatchToolCard[] = [
];
/** 范围选项。 */
export const BATCH_SCOPE_OPTIONS: Array<{ label: string; value: BatchScopeType }> = [
export const BATCH_SCOPE_OPTIONS: Array<{
label: string;
value: BatchScopeType;
}> = [
{ label: '全部商品', value: 'all' },
{ label: '按分类', value: 'category' },
{ label: '手动选择', value: 'manual' },

View File

@@ -1,5 +1,7 @@
import type { Ref } from 'vue';
import type { BatchCategoryOption, BatchProductOption } from '../../types';
import type { ProductPickerItemDto } from '#/api/product';
import type { StoreListItemDto } from '#/api/store';
@@ -11,12 +13,7 @@ import {
} from '#/api/product';
import { getStoreListApi } from '#/api/store';
import {
toBatchCategoryOptions,
toBatchProductOptions,
type BatchCategoryOption,
type BatchProductOption,
} from '../../types';
import { toBatchCategoryOptions, toBatchProductOptions } from '../../types';
interface CreateBatchDataActionsOptions {
categoryOptions: Ref<BatchCategoryOption[]>;

View File

@@ -1,5 +1,7 @@
import type { Ref } from 'vue';
import type { BatchScopeType } from '../../types';
import type {
ProductBatchPricePreviewDto,
ProductBatchScopeDto,
@@ -18,8 +20,6 @@ import {
batchSyncProductStoreApi,
} from '#/api/product';
import type { BatchScopeType } from '../../types';
interface CreateBatchToolActionsOptions {
exportCategoryIds: Ref<string[]>;
exportScopeType: Ref<'all' | 'category'>;
@@ -91,7 +91,7 @@ function decodeBase64ToBlob(base64: string) {
const binary = window.atob(base64);
const bytes = new Uint8Array(binary.length);
for (let index = 0; index < binary.length; index++) {
bytes[index] = binary.charCodeAt(index);
bytes[index] = binary.codePointAt(index) ?? 0;
}
return new Blob([bytes], {
@@ -241,7 +241,9 @@ export function createBatchToolActions(options: CreateBatchToolActionsOptions) {
return;
}
if (options.moveSourceCategoryId.value === options.moveTargetCategoryId.value) {
if (
options.moveSourceCategoryId.value === options.moveTargetCategoryId.value
) {
message.warning('源分类和目标分类不能相同');
return;
}

View File

@@ -1,10 +1,3 @@
import { computed, onActivated, onMounted, ref, watch } from 'vue';
import type { StoreListItemDto } from '#/api/store';
import { BATCH_TOOL_CARDS } from './batch-page/constants';
import { createBatchDataActions } from './batch-page/data-actions';
import { createBatchToolActions } from './batch-page/tool-actions';
import type {
BatchCategoryOption,
BatchScopeType,
@@ -12,6 +5,14 @@ import type {
BatchToolKey,
} from '../types';
import type { StoreListItemDto } from '#/api/store';
import { computed, onActivated, onMounted, ref, watch } from 'vue';
import { BATCH_TOOL_CARDS } from './batch-page/constants';
import { createBatchDataActions } from './batch-page/data-actions';
import { createBatchToolActions } from './batch-page/tool-actions';
/**
* 文件职责:批量工具页面状态编排。
* 1. 管理门店、范围、卡片展开状态。
@@ -23,7 +24,9 @@ export function useProductBatchPage() {
const isStoreLoading = ref(false);
const categoryOptions = ref<BatchCategoryOption[]>([]);
const productOptions = ref<Array<{ label: string; price: number; spuCode: string; value: string }>>([]);
const productOptions = ref<
Array<{ label: string; price: number; spuCode: string; value: string }>
>([]);
const isCategoryLoading = ref(false);
const isProductLoading = ref(false);
@@ -122,7 +125,9 @@ export function useProductBatchPage() {
);
const selectedStoreLabel = computed(() => {
return stores.value.find((item) => item.id === selectedStoreId.value)?.name || '';
return (
stores.value.find((item) => item.id === selectedStoreId.value)?.name || ''
);
});
const scopeEstimatedCount = computed(() => {
@@ -143,8 +148,9 @@ export function useProductBatchPage() {
const moveEstimatedCount = computed(() => {
if (moveSourceCategoryId.value) {
return (
categoryOptions.value.find((item) => item.value === moveSourceCategoryId.value)
?.productCount ?? 0
categoryOptions.value.find(
(item) => item.value === moveSourceCategoryId.value,
)?.productCount ?? 0
);
}

View File

@@ -1,18 +1,20 @@
<script setup lang="ts">
import { Page } from '@vben/common-ui';
import type { BatchToolKey } from './types';
import { computed } from 'vue';
import { Page } from '@vben/common-ui';
import { Alert, Drawer, Empty, Spin } from 'ant-design-vue';
import StoreScopeToolbar from '../../shared/components/StoreScopeToolbar.vue';
import BatchImportExportPanel from './components/BatchImportExportPanel.vue';
import BatchMoveCategoryPanel from './components/BatchMoveCategoryPanel.vue';
import BatchPriceAdjustPanel from './components/BatchPriceAdjustPanel.vue';
import BatchSaleSwitchPanel from './components/BatchSaleSwitchPanel.vue';
import BatchStoreSyncPanel from './components/BatchStoreSyncPanel.vue';
import StoreScopeToolbar from '../../shared/components/StoreScopeToolbar.vue';
import BatchToolCards from './components/BatchToolCards.vue';
import { useProductBatchPage } from './composables/useProductBatchPage';
import type { BatchToolKey } from './types';
const {
activeTool,
@@ -150,7 +152,9 @@ const activeToolTitle = computed(() => {
@update:selected-store-id="setSelectedStoreId"
>
<template #actions>
<div class="pbt-toolbar-tip">选择门店后工具仅作用于当前门店商品</div>
<div class="pbt-toolbar-tip">
选择门店后工具仅作用于当前门店商品
</div>
</template>
</StoreScopeToolbar>

View File

@@ -43,8 +43,8 @@
}
.pbt-card-desc {
margin: 0;
min-height: 40px;
margin: 0;
font-size: 13px;
line-height: 1.5;
color: #9ca3af;

View File

@@ -29,8 +29,8 @@
.pbt-toolbar-tip {
font-size: 13px;
color: #9ca3af;
line-height: 1.4;
color: #9ca3af;
}
.pbt-empty-wrap {

View File

@@ -16,7 +16,6 @@
.page-product-batch,
.pbt-drawer {
.pbt-panel {
padding: 20px;
background: #fff;

View File

@@ -32,6 +32,9 @@ export function buildSkuCombinations(
return;
}
const current = templates[depth];
if (!current) {
return;
}
for (const option of current.options) {
walk(depth + 1, [
...chain,

View File

@@ -16,6 +16,10 @@ interface CreateProductDetailSkuActionsOptions {
specTemplateOptions: Ref<ProductSpecDto[]>;
}
function isDefined<T>(value: null | T | undefined): value is T {
return value !== null && value !== undefined;
}
export function createProductDetailSkuActions(
options: CreateProductDetailSkuActionsOptions,
) {
@@ -87,7 +91,7 @@ export function createProductDetailSkuActions(
function buildSkuRows() {
const selectedTemplates = form.specTemplateIds
.map((id) => specTemplateOptions.value.find((item) => item.id === id))
.filter(Boolean)
.filter((item): item is ProductSpecDto => isDefined(item))
.map((item) => ({
id: item.id,
name: item.name,
@@ -169,19 +173,21 @@ export function createProductDetailSkuActions(
}
function setSkuPrice(index: number, value: number) {
if (index < 0 || index >= form.skus.length) return;
const current = form.skus[index];
if (!current) return;
form.skus[index] = {
...form.skus[index],
price: Number.isFinite(value)
? Math.max(0, value)
: form.skus[index].price,
...current,
price: Number.isFinite(value) ? Math.max(0, value) : current.price,
};
}
function setSkuOriginalPrice(index: number, value: null | number) {
if (index < 0 || index >= form.skus.length) return;
const current = form.skus[index];
if (!current) return;
form.skus[index] = {
...form.skus[index],
...current,
originalPrice:
value !== null && value !== undefined && Number(value) > 0
? Number(value)
@@ -190,17 +196,21 @@ export function createProductDetailSkuActions(
}
function setSkuStock(index: number, value: number) {
if (index < 0 || index >= form.skus.length) return;
const current = form.skus[index];
if (!current) return;
form.skus[index] = {
...form.skus[index],
...current,
stock: Math.max(0, Math.floor(Number(value || 0))),
};
}
function setSkuEnabled(index: number, checked: boolean) {
if (index < 0 || index >= form.skus.length) return;
const current = form.skus[index];
if (!current) return;
form.skus[index] = {
...form.skus[index],
...current,
isEnabled: checked,
};
}

View File

@@ -20,6 +20,10 @@ import { DEFAULT_PRODUCT_DETAIL_FORM } from './product-detail-page/constants';
import { createProductDetailDataActions } from './product-detail-page/data-actions';
import { createProductDetailSkuActions } from './product-detail-page/sku-actions';
function isDefined<T>(value: null | T | undefined): value is T {
return value !== null && value !== undefined;
}
export function useProductDetailPage() {
const route = useRoute();
const router = useRouter();
@@ -71,10 +75,10 @@ export function useProductDetailPage() {
return '#9ca3af';
});
const skuTemplateColumns = computed(() =>
const skuTemplateColumns = computed<ProductSpecDto[]>(() =>
form.specTemplateIds
.map((id) => specTemplateOptions.value.find((item) => item.id === id))
.filter(Boolean),
.filter((item): item is ProductSpecDto => isDefined(item)),
);
const skuActions = createProductDetailSkuActions({

View File

@@ -107,6 +107,36 @@ const sectionList = computed<ProductDetailSectionItem[]>(() => {
const isOnSale = computed(() => form.status === 'on_sale');
const maxUploadReached = computed(() => form.imageUrls.length >= 5);
const skuCountText = computed(() => `${form.skus.length} 个 SKU`);
const originalPriceValue = computed<number | undefined>({
get: () => form.originalPrice ?? undefined,
set: (value) => {
form.originalPrice = value ?? null;
},
});
const warningStockValue = computed<number | undefined>({
get: () => form.warningStock ?? undefined,
set: (value) => {
form.warningStock = value ?? null;
},
});
const packingFeeValue = computed<number | undefined>({
get: () => form.packingFee ?? undefined,
set: (value) => {
form.packingFee = value ?? null;
},
});
const skuBatchPriceValue = computed<number | undefined>({
get: () => skuBatch.price ?? undefined,
set: (value) => {
skuBatch.price = value ?? null;
},
});
const skuBatchStockValue = computed<number | undefined>({
get: () => skuBatch.stock ?? undefined,
set: (value) => {
skuBatch.stock = value ?? null;
},
});
const timedOnShelfValue = computed<Dayjs | undefined>({
get: () =>
@@ -386,7 +416,7 @@ watch(
<div class="pd-pricing-inline">
<span class="pd-pricing-unit">¥</span>
<InputNumber
v-model:value="form.originalPrice"
v-model:value="originalPriceValue"
:min="0"
:precision="2"
:step="0.1"
@@ -418,7 +448,7 @@ watch(
<div class="pd-pricing-inline">
<span class="pd-pricing-unit is-empty"> ¥ </span>
<InputNumber
v-model:value="form.warningStock"
v-model:value="warningStockValue"
:min="0"
:precision="0"
:step="1"
@@ -434,7 +464,7 @@ watch(
<div class="pd-pricing-inline">
<span class="pd-pricing-unit">¥</span>
<InputNumber
v-model:value="form.packingFee"
v-model:value="packingFeeValue"
:min="0"
:precision="2"
:step="0.1"
@@ -478,7 +508,7 @@ watch(
<label>批量设价</label>
<span class="pd-unit">¥</span>
<InputNumber
v-model:value="skuBatch.price"
v-model:value="skuBatchPriceValue"
:min="0"
:precision="2"
:step="0.1"
@@ -489,7 +519,7 @@ watch(
<span class="pd-sku-batch-gap"></span>
<label>批量设库存</label>
<InputNumber
v-model:value="skuBatch.stock"
v-model:value="skuBatchStockValue"
:min="0"
:precision="0"
:step="1"

View File

@@ -38,6 +38,8 @@ export const SCHEDULE_COLOR_PALETTE = [
'#2f54eb',
] as const;
const DEFAULT_SCHEDULE_COLOR = '#1890ff';
/** 创建默认编辑表单。 */
export function createDefaultScheduleForm(): ScheduleEditorForm {
return {
@@ -63,7 +65,7 @@ export function normalizeWeekDays(weekDays: number[]) {
/** 按规则 ID 生成稳定颜色。 */
export function resolveScheduleColor(scheduleId: string) {
if (!scheduleId) {
return SCHEDULE_COLOR_PALETTE[0];
return DEFAULT_SCHEDULE_COLOR;
}
let hash = 0;
@@ -71,5 +73,8 @@ export function resolveScheduleColor(scheduleId: string) {
hash = (hash * 31 + (char.codePointAt(0) ?? 0)) >>> 0;
}
return SCHEDULE_COLOR_PALETTE[hash % SCHEDULE_COLOR_PALETTE.length];
return (
SCHEDULE_COLOR_PALETTE[hash % SCHEDULE_COLOR_PALETTE.length] ??
DEFAULT_SCHEDULE_COLOR
);
}

View File

@@ -99,8 +99,8 @@ export function useProductSchedulePage() {
.map((item) => buildTimelineRow(item, getRuleColor(item.id)));
});
function getRuleColor(scheduleId: string) {
return resolveScheduleColor(scheduleId);
function getRuleColor(scheduleId: string | undefined) {
return resolveScheduleColor(scheduleId || '');
}
function resolveProductName(productId: string) {