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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@
import type { BatchToolCard, BatchToolKey } from '../types'; import type { BatchToolCard, BatchToolKey } from '../types';
import { IconifyIcon } from '@vben/icons'; import { IconifyIcon } from '@vben/icons';
import { Button } from 'ant-design-vue'; import { Button } from 'ant-design-vue';
interface Props { interface Props {
@@ -41,4 +42,3 @@ const emit = defineEmits<Emits>();
</article> </article>
</div> </div>
</template> </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: 'all' },
{ label: '按分类', value: 'category' }, { label: '按分类', value: 'category' },
{ label: '手动选择', value: 'manual' }, { label: '手动选择', value: 'manual' },

View File

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

View File

@@ -1,5 +1,7 @@
import type { Ref } from 'vue'; import type { Ref } from 'vue';
import type { BatchScopeType } from '../../types';
import type { import type {
ProductBatchPricePreviewDto, ProductBatchPricePreviewDto,
ProductBatchScopeDto, ProductBatchScopeDto,
@@ -18,8 +20,6 @@ import {
batchSyncProductStoreApi, batchSyncProductStoreApi,
} from '#/api/product'; } from '#/api/product';
import type { BatchScopeType } from '../../types';
interface CreateBatchToolActionsOptions { interface CreateBatchToolActionsOptions {
exportCategoryIds: Ref<string[]>; exportCategoryIds: Ref<string[]>;
exportScopeType: Ref<'all' | 'category'>; exportScopeType: Ref<'all' | 'category'>;
@@ -91,7 +91,7 @@ function decodeBase64ToBlob(base64: string) {
const binary = window.atob(base64); const binary = window.atob(base64);
const bytes = new Uint8Array(binary.length); const bytes = new Uint8Array(binary.length);
for (let index = 0; index < binary.length; index++) { for (let index = 0; index < binary.length; index++) {
bytes[index] = binary.charCodeAt(index); bytes[index] = binary.codePointAt(index) ?? 0;
} }
return new Blob([bytes], { return new Blob([bytes], {
@@ -241,7 +241,9 @@ export function createBatchToolActions(options: CreateBatchToolActionsOptions) {
return; return;
} }
if (options.moveSourceCategoryId.value === options.moveTargetCategoryId.value) { if (
options.moveSourceCategoryId.value === options.moveTargetCategoryId.value
) {
message.warning('源分类和目标分类不能相同'); message.warning('源分类和目标分类不能相同');
return; 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 { import type {
BatchCategoryOption, BatchCategoryOption,
BatchScopeType, BatchScopeType,
@@ -12,6 +5,14 @@ import type {
BatchToolKey, BatchToolKey,
} from '../types'; } 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. 管理门店、范围、卡片展开状态。 * 1. 管理门店、范围、卡片展开状态。
@@ -23,7 +24,9 @@ export function useProductBatchPage() {
const isStoreLoading = ref(false); const isStoreLoading = ref(false);
const categoryOptions = ref<BatchCategoryOption[]>([]); 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 isCategoryLoading = ref(false);
const isProductLoading = ref(false); const isProductLoading = ref(false);
@@ -122,7 +125,9 @@ export function useProductBatchPage() {
); );
const selectedStoreLabel = computed(() => { 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(() => { const scopeEstimatedCount = computed(() => {
@@ -143,8 +148,9 @@ export function useProductBatchPage() {
const moveEstimatedCount = computed(() => { const moveEstimatedCount = computed(() => {
if (moveSourceCategoryId.value) { if (moveSourceCategoryId.value) {
return ( return (
categoryOptions.value.find((item) => item.value === moveSourceCategoryId.value) categoryOptions.value.find(
?.productCount ?? 0 (item) => item.value === moveSourceCategoryId.value,
)?.productCount ?? 0
); );
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,6 +16,10 @@ interface CreateProductDetailSkuActionsOptions {
specTemplateOptions: Ref<ProductSpecDto[]>; specTemplateOptions: Ref<ProductSpecDto[]>;
} }
function isDefined<T>(value: null | T | undefined): value is T {
return value !== null && value !== undefined;
}
export function createProductDetailSkuActions( export function createProductDetailSkuActions(
options: CreateProductDetailSkuActionsOptions, options: CreateProductDetailSkuActionsOptions,
) { ) {
@@ -87,7 +91,7 @@ export function createProductDetailSkuActions(
function buildSkuRows() { function buildSkuRows() {
const selectedTemplates = form.specTemplateIds const selectedTemplates = form.specTemplateIds
.map((id) => specTemplateOptions.value.find((item) => item.id === id)) .map((id) => specTemplateOptions.value.find((item) => item.id === id))
.filter(Boolean) .filter((item): item is ProductSpecDto => isDefined(item))
.map((item) => ({ .map((item) => ({
id: item.id, id: item.id,
name: item.name, name: item.name,
@@ -169,19 +173,21 @@ export function createProductDetailSkuActions(
} }
function setSkuPrice(index: number, value: number) { 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] = {
...form.skus[index], ...current,
price: Number.isFinite(value) price: Number.isFinite(value) ? Math.max(0, value) : current.price,
? Math.max(0, value)
: form.skus[index].price,
}; };
} }
function setSkuOriginalPrice(index: number, value: null | number) { 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] = {
...form.skus[index], ...current,
originalPrice: originalPrice:
value !== null && value !== undefined && Number(value) > 0 value !== null && value !== undefined && Number(value) > 0
? Number(value) ? Number(value)
@@ -190,17 +196,21 @@ export function createProductDetailSkuActions(
} }
function setSkuStock(index: number, value: number) { 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] = {
...form.skus[index], ...current,
stock: Math.max(0, Math.floor(Number(value || 0))), stock: Math.max(0, Math.floor(Number(value || 0))),
}; };
} }
function setSkuEnabled(index: number, checked: boolean) { 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] = {
...form.skus[index], ...current,
isEnabled: checked, 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 { createProductDetailDataActions } from './product-detail-page/data-actions';
import { createProductDetailSkuActions } from './product-detail-page/sku-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() { export function useProductDetailPage() {
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
@@ -71,10 +75,10 @@ export function useProductDetailPage() {
return '#9ca3af'; return '#9ca3af';
}); });
const skuTemplateColumns = computed(() => const skuTemplateColumns = computed<ProductSpecDto[]>(() =>
form.specTemplateIds form.specTemplateIds
.map((id) => specTemplateOptions.value.find((item) => item.id === id)) .map((id) => specTemplateOptions.value.find((item) => item.id === id))
.filter(Boolean), .filter((item): item is ProductSpecDto => isDefined(item)),
); );
const skuActions = createProductDetailSkuActions({ const skuActions = createProductDetailSkuActions({

View File

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

View File

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