add product sku
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<gao-chart-card
|
||||
title="履约进度排行"
|
||||
caption="购买数量的当前履约快照"
|
||||
caption="基础单位购买数量的当前履约快照"
|
||||
:option="chartOption"
|
||||
:loading="loading"
|
||||
:has-data="list.length > 0"
|
||||
@@ -33,19 +33,19 @@ const chartOption = computed<EChartsOption>(() => {
|
||||
name: "待履约",
|
||||
type: "bar",
|
||||
stack: "progress",
|
||||
data: list.map(item => getNumber(item.reservedQuantity))
|
||||
data: list.map(item => getNumber(item.reservedBaseQuantity))
|
||||
},
|
||||
{
|
||||
name: "已履约",
|
||||
type: "bar",
|
||||
stack: "progress",
|
||||
data: list.map(item => getNumber(item.fulfilledQuantity))
|
||||
data: list.map(item => getNumber(item.fulfilledBaseQuantity))
|
||||
},
|
||||
{
|
||||
name: "可履约",
|
||||
type: "bar",
|
||||
stack: "progress",
|
||||
data: list.map(item => getNumber(item.availableQuantity))
|
||||
data: list.map(item => getNumber(item.availableBaseQuantity))
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<gao-chart-card
|
||||
title="每日履约趋势"
|
||||
caption="冲正数量允许为负值"
|
||||
caption="基础单位,冲正数量允许为负值"
|
||||
:date-text-list="dateTextList"
|
||||
:option="buildOption"
|
||||
:loading="loading"
|
||||
@@ -41,16 +41,16 @@ function buildOption(visibleDateTextList: string[]): EChartsOption {
|
||||
name: "直接履约",
|
||||
type: "bar",
|
||||
stack: "fulfillment",
|
||||
data: visibleDateTextList.map(dateText => getNumber(getDailyItem(props.list, dateText)?.directQuantity))
|
||||
data: visibleDateTextList.map(dateText => getNumber(getDailyItem(props.list, dateText)?.directBaseQuantity))
|
||||
},
|
||||
{
|
||||
name: "套餐履约",
|
||||
type: "bar",
|
||||
stack: "fulfillment",
|
||||
data: visibleDateTextList.map(dateText => getNumber(getDailyItem(props.list, dateText)?.packageQuantity))
|
||||
data: visibleDateTextList.map(dateText => getNumber(getDailyItem(props.list, dateText)?.packageBaseQuantity))
|
||||
},
|
||||
{
|
||||
name: "基础单位变化",
|
||||
name: "履约基础单位",
|
||||
type: "line",
|
||||
smooth: true,
|
||||
data: visibleDateTextList.map(dateText => getNumber(getDailyItem(props.list, dateText)?.baseQuantity))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<gao-chart-card
|
||||
title="产品销售排行"
|
||||
caption="按销量取前 10"
|
||||
caption="按基础单位销量取前 10"
|
||||
:option="chartOption"
|
||||
:loading="loading"
|
||||
:has-data="list.length > 0"
|
||||
@@ -33,13 +33,13 @@ const chartOption = computed<EChartsOption>(() => {
|
||||
name: "直接销量",
|
||||
type: "bar",
|
||||
stack: "quantity",
|
||||
data: list.map(item => getNumber(item.directQuantity))
|
||||
data: list.map(item => getNumber(item.directBaseQuantity))
|
||||
},
|
||||
{
|
||||
name: "套餐权益",
|
||||
type: "bar",
|
||||
stack: "quantity",
|
||||
data: list.map(item => getNumber(item.packageQuantity))
|
||||
data: list.map(item => getNumber(item.packageBaseQuantity))
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<gao-chart-card
|
||||
title="产品销售趋势"
|
||||
caption="直接产品与套餐权益分开展示"
|
||||
caption="基础单位,直接产品与套餐权益分开展示"
|
||||
:date-text-list="dateTextList"
|
||||
:option="buildOption"
|
||||
:loading="loading"
|
||||
@@ -41,13 +41,13 @@ function buildOption(visibleDateTextList: string[]): EChartsOption {
|
||||
name: "直接销量",
|
||||
type: "bar",
|
||||
stack: "quantity",
|
||||
data: visibleDateTextList.map(dateText => getNumber(getDailyItem(props.list, dateText)?.directQuantity))
|
||||
data: visibleDateTextList.map(dateText => getNumber(getDailyItem(props.list, dateText)?.directBaseQuantity))
|
||||
},
|
||||
{
|
||||
name: "套餐权益",
|
||||
type: "bar",
|
||||
stack: "quantity",
|
||||
data: visibleDateTextList.map(dateText => getNumber(getDailyItem(props.list, dateText)?.packageQuantity))
|
||||
data: visibleDateTextList.map(dateText => getNumber(getDailyItem(props.list, dateText)?.packageBaseQuantity))
|
||||
},
|
||||
{
|
||||
name: "订单数",
|
||||
|
||||
@@ -0,0 +1,415 @@
|
||||
<template>
|
||||
<t-swipe-cell class="sku-cell">
|
||||
<t-cell role="button" tabindex="0" arrow @click="openEditor">
|
||||
<template #title>
|
||||
<div class="title">
|
||||
<span class="code gray" v-text="sku.code" />
|
||||
<strong class="clip-text" v-text="skuTitle" />
|
||||
<t-tag v-if="sku.id" size="small" variant="light-outline" :content="sku.status === 'ACTIVE' ? '已建立' : '已停用'" />
|
||||
</div>
|
||||
</template>
|
||||
<template #note>
|
||||
<t-tag
|
||||
size="small"
|
||||
variant="light-outline"
|
||||
:theme="sku.status === 'ACTIVE' ? 'success' : 'warning'"
|
||||
:content="sku.status === 'ACTIVE' ? '启用' : '停用'"
|
||||
/>
|
||||
</template>
|
||||
<template #description>
|
||||
<div class="tags">
|
||||
<t-tag
|
||||
v-for="entry in skuSpecEntries"
|
||||
:key="`${entry.specName}-${entry.valueName}`"
|
||||
size="small"
|
||||
:content="entry.valueName"
|
||||
/>
|
||||
<t-tag
|
||||
v-if="sku.stockEnabled"
|
||||
theme="warning"
|
||||
size="small"
|
||||
content="库存"
|
||||
/>
|
||||
<t-tag
|
||||
v-for="unit in sku.unitList"
|
||||
:key="unit.clientKey"
|
||||
class="description-tag"
|
||||
size="small"
|
||||
:content="unit.name || '未命名单位'"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</t-cell>
|
||||
<template #right>
|
||||
<t-button class="delete-action" theme="danger" @click.stop="requestRemove">
|
||||
删除
|
||||
</t-button>
|
||||
</template>
|
||||
</t-swipe-cell>
|
||||
|
||||
<bottom-popup
|
||||
v-model="editorVisible"
|
||||
title="配置 SKU 明细"
|
||||
height="min(90vh, 52rem)"
|
||||
:left-button-text="sku.id ? '关闭' : '取消'"
|
||||
right-button-text="完成"
|
||||
:z-index="skuEditorZIndex"
|
||||
:overlay-props="{ zIndex: skuEditorZIndex - 1 }"
|
||||
@left-button-click="closeEditor"
|
||||
@right-button-click="closeEditor"
|
||||
>
|
||||
<div class="sku-popup">
|
||||
<div class="sku-popup-header">
|
||||
<div class="sku-heading">
|
||||
<span class="sku-index" v-text="String(props.skuIndex + 1).padStart(2, '0')" />
|
||||
<strong v-text="skuTitle" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sku-spec-panel">
|
||||
<div class="section-title">
|
||||
<strong>销售规格</strong>
|
||||
<span v-if="sku.specSummary" v-text="`摘要:${sku.specSummary}`" />
|
||||
</div>
|
||||
<div v-if="skuSpecEntries.length" class="sku-spec-list">
|
||||
<div v-for="entry in skuSpecEntries" :key="`${entry.specName}-${entry.valueName}`" class="sku-spec-item">
|
||||
<span class="spec-name" v-text="entry.specName" />
|
||||
<strong v-text="entry.valueName" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="sku-spec-empty" v-text="props.specList.length ? '请重新生成 SKU 明细以匹配规格项和规格值' : '无多规格产品,使用默认 SKU'" />
|
||||
</div>
|
||||
|
||||
<t-form class="sku-form" label-align="left" label-width="5rem">
|
||||
<t-form-item label="SKU 编码" required-mark help="门店内唯一;已有 SKU 修改失败时请保留原编码">
|
||||
<t-input v-model.trim="sku.code" borderless maxlength="64" placeholder="如 TSHIRT-RED-M" />
|
||||
</t-form-item>
|
||||
<t-form-item label="明细名称" required-mark>
|
||||
<t-input v-model.trim="sku.name" borderless maxlength="128" placeholder="如 黑色运动鞋;男;36" />
|
||||
</t-form-item>
|
||||
<t-form-item label="SKU 条码">
|
||||
<t-input v-model.trim="sku.barcode" borderless maxlength="64" placeholder="选填" />
|
||||
</t-form-item>
|
||||
<div class="section-title setting-title">
|
||||
<strong>库存设置</strong>
|
||||
<span>每条 SKU 明细独立生效</span>
|
||||
</div>
|
||||
<t-form-item label="库存管理" content-align="right">
|
||||
<t-switch v-model="sku.stockEnabled" size="small" :disabled="props.productType === 'SERVICE'" />
|
||||
</t-form-item>
|
||||
<t-form-item label="允许小数" content-align="right">
|
||||
<t-switch v-model="sku.allowDecimalQuantity" size="small" />
|
||||
</t-form-item>
|
||||
<t-form-item v-if="sku.stockEnabled" label="最低库存" help="基础单位数量,最多三位小数">
|
||||
<t-input v-model.trim="sku.minStockQuantityText" type="number" align="right" borderless placeholder="0" />
|
||||
</t-form-item>
|
||||
<t-form-item label="明细状态" content-align="right">
|
||||
<t-switch :model-value="sku.status === 'ACTIVE'" size="small" @change="setSkuStatus" />
|
||||
</t-form-item>
|
||||
<t-form-item label="备注">
|
||||
<t-input v-model.trim="sku.remark" borderless maxlength="255" placeholder="选填" />
|
||||
</t-form-item>
|
||||
</t-form>
|
||||
|
||||
<photo-scroller
|
||||
:model-value="photoUrlList"
|
||||
v-model:photo-items="sku.photoItemList"
|
||||
title="SKU 图片(选填)"
|
||||
:edit-mode="true"
|
||||
:upload="uploadPhotoList"
|
||||
/>
|
||||
|
||||
<div class="unit-section">
|
||||
<div class="unit-heading">
|
||||
<strong>销售单位</strong>
|
||||
<span>采购、销售和库存操作共用</span>
|
||||
</div>
|
||||
<div class="unit-summary-list">
|
||||
<div v-for="unit in sku.unitList" :key="unit.clientKey" class="unit-summary">
|
||||
<strong v-text="unit.name || '未命名单位'" />
|
||||
<span v-text="getUnitSummary(unit)" />
|
||||
</div>
|
||||
</div>
|
||||
<t-button class="unit-button" variant="dashed" size="small" content="配置销售单位" @click="requestConfigureUnits" />
|
||||
</div>
|
||||
</div>
|
||||
</bottom-popup>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { BottomPopup } from "timi-tdesign-mobile";
|
||||
import type { ProductType } from "@/types/Product";
|
||||
import type { ProductSkuForm, ProductSpecForm, ProductSkuUnitForm } from "@/types/ProductEditor";
|
||||
import PhotoScroller from "@/components/PhotoScroller.vue";
|
||||
import { getProductSkuSpecEntries } from "@/utils/product";
|
||||
import { uploadPhotoList } from "@/utils/photo";
|
||||
|
||||
defineOptions({ name: "ProductSkuEditor" });
|
||||
|
||||
const props = defineProps<{
|
||||
productType: ProductType;
|
||||
skuIndex: number;
|
||||
specList: ProductSpecForm[];
|
||||
}>();
|
||||
|
||||
const sku = defineModel<ProductSkuForm>("sku", { required: true });
|
||||
const emit = defineEmits<{
|
||||
configureUnits: [];
|
||||
remove: [];
|
||||
}>();
|
||||
|
||||
const editorVisible = ref(false);
|
||||
const skuEditorZIndex = 12000;
|
||||
const photoUrlList = computed(() => sku.value.photoItemList.map(item => item.url));
|
||||
const skuSpecEntries = computed(() => getProductSkuSpecEntries(sku.value, props.specList));
|
||||
const skuTitle = computed(() => props.specList.length ? `SKU 明细 ${props.skuIndex + 1}` : "默认 SKU");
|
||||
|
||||
function openEditor(): void {
|
||||
editorVisible.value = true;
|
||||
}
|
||||
|
||||
function closeEditor(): void {
|
||||
editorVisible.value = false;
|
||||
}
|
||||
|
||||
function requestRemove(): void {
|
||||
editorVisible.value = false;
|
||||
emit("remove");
|
||||
}
|
||||
|
||||
function requestConfigureUnits(): void {
|
||||
emit("configureUnits");
|
||||
}
|
||||
|
||||
function setSkuStatus(value: boolean | string | number): void {
|
||||
const active = value === true || value === "true" || value === 1 || value === "1";
|
||||
sku.value.status = active ? "ACTIVE" : "INACTIVE";
|
||||
}
|
||||
|
||||
function getUnitSummary(unit: ProductSkuUnitForm): string {
|
||||
const quantityText = unit.isBase ? "基础单位" : `1 ${unit.name || "单位"} = ${unit.baseQuantityPerUnit || "?"} 基础单位`;
|
||||
const priceText = unit.salePriceText ? `¥${unit.salePriceText}` : "未定价";
|
||||
const saleText = unit.allowSale ? "可销售" : "停售";
|
||||
const purchaseText = unit.allowPurchase ? "可采购" : "禁采购";
|
||||
return `${quantityText} · ${priceText} · ${saleText} · ${purchaseText}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.sku-cell {
|
||||
|
||||
.title {
|
||||
|
||||
}
|
||||
|
||||
.cell-title {
|
||||
gap: .5rem;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
strong {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.description-tags {
|
||||
gap: .375rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.description-tag {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.delete-action {
|
||||
width: 4.5rem;
|
||||
height: 100%;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.sku-popup {
|
||||
max-height: 100%;
|
||||
padding: 1rem 0 calc(1rem + env(safe-area-inset-bottom, 0px));
|
||||
overflow-y: auto;
|
||||
|
||||
.sku-popup-header {
|
||||
gap: .5rem;
|
||||
display: flex;
|
||||
padding: 0 1rem .75rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.sku-heading {
|
||||
gap: .5rem;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.sku-index {
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
color: var(--td-brand-color);
|
||||
flex: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--td-brand-color-1);
|
||||
border-radius: .625rem;
|
||||
font-size: .6875rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.sku-spec-panel {
|
||||
margin: 0 .75rem .75rem;
|
||||
padding: .75rem;
|
||||
background: var(--td-bg-color-secondarycontainer);
|
||||
border-radius: .875rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
gap: .5rem;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
|
||||
span {
|
||||
color: var(--td-text-color-secondary);
|
||||
font-size: .75rem;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.sku-spec-list {
|
||||
gap: .5rem;
|
||||
display: grid;
|
||||
margin-top: .625rem;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.sku-spec-item {
|
||||
gap: .375rem;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
padding: .5rem .625rem;
|
||||
align-items: baseline;
|
||||
background: var(--td-bg-color-container);
|
||||
border-radius: .625rem;
|
||||
|
||||
.spec-name {
|
||||
color: var(--td-text-color-secondary);
|
||||
font-size: .75rem;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
strong {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.sku-spec-empty {
|
||||
margin-top: .5rem;
|
||||
color: var(--td-text-color-secondary);
|
||||
font-size: .75rem;
|
||||
}
|
||||
|
||||
.sku-form {
|
||||
margin: 0 .75rem;
|
||||
|
||||
.setting-title {
|
||||
margin-top: .5rem;
|
||||
padding: .75rem 0 .25rem;
|
||||
border-top: 1px solid var(--td-component-border);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.t-form-item) {
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
:deep(.photo-scroller) {
|
||||
margin: .5rem .75rem 0;
|
||||
}
|
||||
|
||||
.unit-section {
|
||||
margin-top: .75rem;
|
||||
border-top: 1px solid var(--td-component-border);
|
||||
}
|
||||
|
||||
.unit-heading {
|
||||
gap: .5rem;
|
||||
display: flex;
|
||||
padding: .75rem 1rem .5rem;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
|
||||
span {
|
||||
color: var(--td-text-color-secondary);
|
||||
font-size: .75rem;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.unit-summary-list {
|
||||
gap: .5rem;
|
||||
display: flex;
|
||||
padding: 0 1rem .5rem;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.unit-summary {
|
||||
gap: .5rem;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
padding: .625rem .75rem;
|
||||
align-items: center;
|
||||
background: var(--td-bg-color-secondarycontainer);
|
||||
border-radius: .75rem;
|
||||
justify-content: space-between;
|
||||
|
||||
strong {
|
||||
max-width: 5rem;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
span {
|
||||
min-width: 0;
|
||||
color: var(--td-text-color-secondary);
|
||||
font-size: .75rem;
|
||||
text-align: right;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.unit-button {
|
||||
margin: 0 1rem .75rem;
|
||||
}
|
||||
}
|
||||
|
||||
:global(html.theme-dark) {
|
||||
.sku-popup {
|
||||
.sku-spec-panel,
|
||||
.unit-summary {
|
||||
background: rgba(148, 163, 184, .12);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,198 @@
|
||||
<template>
|
||||
<bottom-popup
|
||||
v-model="visible"
|
||||
title="统一配置 SKU 单位"
|
||||
height="min(88vh, 48rem)"
|
||||
left-button-text="关闭"
|
||||
right-button-text="应用到全部"
|
||||
:z-index="batchEditorZIndex"
|
||||
:overlay-props="{ zIndex: batchEditorZIndex - 1 }"
|
||||
@left-button-click="close"
|
||||
@right-button-click="applyUnits"
|
||||
>
|
||||
<div class="batch-editor">
|
||||
<div class="editor-context">
|
||||
<strong>批量配置销售单位</strong>
|
||||
<span v-text="`以下单位将应用到全部 ${props.skuCount} 个 SKU 明细`" />
|
||||
</div>
|
||||
|
||||
<div class="unit-list">
|
||||
<div v-for="(unit, unitIndex) in unitList" :key="unit.clientKey" class="unit-card">
|
||||
<div class="unit-card-header">
|
||||
<strong v-text="`单位 ${unitIndex + 1}`" />
|
||||
<t-button v-if="1 < unitList.length" theme="danger" variant="text" size="extra-small" content="删除" @click="removeUnit(unitIndex)" />
|
||||
</div>
|
||||
<t-form class="unit-form" label-align="left" label-width="6rem">
|
||||
<t-form-item label="单位名称" required-mark>
|
||||
<t-input v-model.trim="unit.name" borderless maxlength="32" placeholder="如 件、盒、箱" />
|
||||
</t-form-item>
|
||||
<t-form-item label="基础单位" content-align="right">
|
||||
<t-switch v-model="unit.isBase" size="small" :disabled="unitList.length === 1" @change="setBaseUnit(unitIndex)" />
|
||||
</t-form-item>
|
||||
<t-form-item label="换算数量" help="1 个当前单位对应的基础单位数量" required-mark>
|
||||
<t-input v-model.trim="unit.baseQuantityPerUnit" type="number" align="right" borderless :disabled="unit.isBase" placeholder="1" />
|
||||
</t-form-item>
|
||||
<t-form-item label="售价(元)">
|
||||
<t-input v-model.trim="unit.salePriceText" type="number" align="right" borderless placeholder="选填" />
|
||||
</t-form-item>
|
||||
<t-form-item label="允许采购" content-align="right">
|
||||
<t-switch v-model="unit.allowPurchase" size="small" :disabled="props.productType === 'SERVICE'" />
|
||||
</t-form-item>
|
||||
<t-form-item label="允许销售" content-align="right">
|
||||
<t-switch v-model="unit.allowSale" size="small" />
|
||||
</t-form-item>
|
||||
</t-form>
|
||||
</div>
|
||||
<t-button class="add-button" variant="dashed" size="small" content="添加销售单位" @click="addUnit" />
|
||||
</div>
|
||||
</div>
|
||||
</bottom-popup>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { BottomPopup } from "timi-tdesign-mobile";
|
||||
import type { ProductType } from "@/types/Product";
|
||||
import type { ProductSkuUnitForm } from "@/types/ProductEditor";
|
||||
import { Message } from "@/utils/message";
|
||||
|
||||
defineOptions({ name: "ProductSkuUnitBatchEditor" });
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean;
|
||||
productType: ProductType;
|
||||
skuCount: number;
|
||||
}>();
|
||||
|
||||
const unitList = defineModel<ProductSkuUnitForm[]>("unitList", { required: true });
|
||||
const emit = defineEmits<{
|
||||
"update:modelValue": [value: boolean];
|
||||
apply: [];
|
||||
close: [];
|
||||
}>();
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: value => handleVisibleChange(value)
|
||||
});
|
||||
const batchEditorZIndex = 12000;
|
||||
|
||||
function handleVisibleChange(value: boolean): void {
|
||||
emit("update:modelValue", value);
|
||||
if (!value) {
|
||||
emit("close");
|
||||
}
|
||||
}
|
||||
|
||||
function close(): void {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
function applyUnits(): void {
|
||||
if (!unitList.value.length) {
|
||||
Message.warning("至少需要配置一个销售单位");
|
||||
return;
|
||||
}
|
||||
emit("apply");
|
||||
close();
|
||||
}
|
||||
|
||||
function addUnit(): void {
|
||||
unitList.value.push({
|
||||
clientKey: window.crypto.randomUUID(),
|
||||
name: unitList.value[0]?.name || "件",
|
||||
baseQuantityPerUnit: "",
|
||||
salePriceText: "",
|
||||
isBase: false,
|
||||
allowPurchase: props.productType !== "SERVICE",
|
||||
allowSale: true,
|
||||
sort: unitList.value.length
|
||||
});
|
||||
}
|
||||
|
||||
function removeUnit(index: number): void {
|
||||
if (unitList.value.length < 2) {
|
||||
Message.warning("每个 SKU 至少需要一个单位");
|
||||
return;
|
||||
}
|
||||
const wasBase = unitList.value[index]?.isBase;
|
||||
unitList.value.splice(index, 1);
|
||||
if (wasBase) {
|
||||
setBaseUnit(0);
|
||||
}
|
||||
}
|
||||
|
||||
function setBaseUnit(index: number): void {
|
||||
unitList.value.forEach((unit, unitIndex) => { unit.isBase = unitIndex === index; });
|
||||
const unit = unitList.value[index];
|
||||
if (unit) {
|
||||
unit.baseQuantityPerUnit = "1";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.batch-editor {
|
||||
max-height: 100%;
|
||||
overflow-y: auto;
|
||||
padding: 1rem 0 calc(1rem + env(safe-area-inset-bottom, 0px));
|
||||
|
||||
.editor-context {
|
||||
gap: .25rem;
|
||||
display: flex;
|
||||
margin: 0 1rem .75rem;
|
||||
padding: .75rem;
|
||||
background: var(--td-bg-color-secondarycontainer);
|
||||
border-radius: .875rem;
|
||||
flex-direction: column;
|
||||
|
||||
span {
|
||||
color: var(--td-text-color-secondary);
|
||||
font-size: .75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.unit-list {
|
||||
gap: .75rem;
|
||||
display: flex;
|
||||
padding: 0 1rem 1rem;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.unit-card {
|
||||
border: 1px solid var(--td-component-border);
|
||||
padding: .5rem 0;
|
||||
border-radius: .75rem;
|
||||
}
|
||||
|
||||
.unit-card-header {
|
||||
gap: .5rem;
|
||||
display: flex;
|
||||
padding: 0 .75rem .5rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.unit-form {
|
||||
margin: 0 .75rem;
|
||||
|
||||
:deep(.t-form-item) {
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.add-button {
|
||||
align-self: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
:global(html.theme-dark) .batch-editor {
|
||||
.editor-context {
|
||||
background: rgba(148, 163, 184, .12);
|
||||
}
|
||||
|
||||
.unit-card {
|
||||
border-color: rgba(148, 163, 184, .3);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,252 @@
|
||||
<template>
|
||||
<bottom-popup
|
||||
v-model="visible"
|
||||
title="配置 SKU 单位"
|
||||
height="min(88vh, 48rem)"
|
||||
left-button-text="关闭"
|
||||
right-button-text="完成"
|
||||
:z-index="unitEditorZIndex"
|
||||
:overlay-props="{ zIndex: unitEditorZIndex - 1 }"
|
||||
@left-button-click="close"
|
||||
@right-button-click="close"
|
||||
>
|
||||
<div v-if="sku" class="unit-editor">
|
||||
<div class="editor-context">
|
||||
<span class="context-label">当前 SKU 明细</span>
|
||||
<strong class="context-title" v-text="sku.name || sku.specSummary || '默认 SKU'" />
|
||||
<div v-if="skuSpecEntries.length" class="context-spec-list">
|
||||
<div v-for="entry in skuSpecEntries" :key="`${entry.specName}-${entry.valueName}`" class="context-spec">
|
||||
<span v-text="entry.specName" />
|
||||
<strong v-text="entry.valueName" />
|
||||
</div>
|
||||
</div>
|
||||
<span v-else class="context-empty">无多规格</span>
|
||||
<div class="context-count" v-text="`${sku.unitList.length} 个单位`" />
|
||||
</div>
|
||||
|
||||
<div class="unit-list">
|
||||
<div v-for="(unit, unitIndex) in sku.unitList" :key="unit.clientKey" class="unit-card">
|
||||
<div class="unit-card-header">
|
||||
<strong v-text="`单位 ${unitIndex + 1}`" />
|
||||
<t-button v-if="1 < sku.unitList.length" theme="danger" variant="text" size="extra-small" content="删除" @click="removeUnit(unitIndex)" />
|
||||
</div>
|
||||
<t-form class="unit-form" label-align="left" label-width="6rem">
|
||||
<t-form-item label="单位名称" required-mark>
|
||||
<t-input v-model.trim="unit.name" borderless maxlength="32" placeholder="如 件、盒、箱" />
|
||||
</t-form-item>
|
||||
<t-form-item label="基础单位" content-align="right">
|
||||
<t-switch v-model="unit.isBase" size="small" :disabled="sku.unitList.length === 1" @change="setBaseUnit(unitIndex)" />
|
||||
</t-form-item>
|
||||
<t-form-item label="换算数量" help="1 个当前单位对应的基础单位数量" required-mark>
|
||||
<t-input v-model.trim="unit.baseQuantityPerUnit" type="number" align="right" borderless :disabled="unit.isBase" placeholder="1" />
|
||||
</t-form-item>
|
||||
<t-form-item label="售价(元)">
|
||||
<t-input v-model.trim="unit.salePriceText" type="number" align="right" borderless placeholder="选填" />
|
||||
</t-form-item>
|
||||
<t-form-item label="允许采购" content-align="right">
|
||||
<t-switch v-model="unit.allowPurchase" size="small" :disabled="props.productType === 'SERVICE'" />
|
||||
</t-form-item>
|
||||
<t-form-item label="允许销售" content-align="right">
|
||||
<t-switch v-model="unit.allowSale" size="small" />
|
||||
</t-form-item>
|
||||
</t-form>
|
||||
</div>
|
||||
<t-button class="add-button" variant="dashed" size="small" content="添加 SKU 单位" @click="addUnit" />
|
||||
</div>
|
||||
</div>
|
||||
</bottom-popup>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { BottomPopup } from "timi-tdesign-mobile";
|
||||
import type { ProductSkuUnit, ProductType } from "@/types/Product";
|
||||
import type { ProductSkuForm, ProductSkuUnitForm, ProductSpecForm } from "@/types/ProductEditor";
|
||||
import { Message } from "@/utils/message";
|
||||
import { formatProductAmount, getProductSkuSpecEntries } from "@/utils/product";
|
||||
|
||||
defineOptions({ name: "ProductSkuUnitEditor" });
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean;
|
||||
productType: ProductType;
|
||||
specList: ProductSpecForm[];
|
||||
}>();
|
||||
|
||||
const sku = defineModel<ProductSkuForm | undefined>("sku");
|
||||
|
||||
const emit = defineEmits<{
|
||||
"update:modelValue": [value: boolean];
|
||||
close: [];
|
||||
}>();
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: value => handleVisibleChange(value)
|
||||
});
|
||||
const unitEditorZIndex = 12100;
|
||||
const skuSpecEntries = computed(() => sku.value ? getProductSkuSpecEntries(sku.value, props.specList) : []);
|
||||
|
||||
function handleVisibleChange(value: boolean): void {
|
||||
emit("update:modelValue", value);
|
||||
if (!value) {
|
||||
emit("close");
|
||||
}
|
||||
}
|
||||
|
||||
function close(): void {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
function createUnit(unit?: Partial<ProductSkuUnit>, defaultName = "件"): ProductSkuUnitForm {
|
||||
return {
|
||||
...unit,
|
||||
id: unit?.id,
|
||||
clientKey: window.crypto.randomUUID(),
|
||||
name: unit?.name || defaultName,
|
||||
baseQuantityPerUnit: unit?.baseQuantityPerUnit ?? "1",
|
||||
salePriceText: unit?.salePrice === null || unit?.salePrice === undefined ? "" : formatProductAmount(unit.salePrice),
|
||||
isBase: unit?.isBase ?? true,
|
||||
allowPurchase: unit?.allowPurchase ?? props.productType !== "SERVICE",
|
||||
allowSale: unit?.allowSale ?? true,
|
||||
sort: unit?.sort ?? 0
|
||||
};
|
||||
}
|
||||
|
||||
function addUnit(): void {
|
||||
if (!sku.value) {
|
||||
return;
|
||||
}
|
||||
sku.value.unitList.push(createUnit({ isBase: false, baseQuantityPerUnit: "" }, sku.value.unitList[0]?.name || "件"));
|
||||
}
|
||||
|
||||
function removeUnit(index: number): void {
|
||||
if (!sku.value) {
|
||||
return;
|
||||
}
|
||||
if (sku.value.unitList.length < 2) {
|
||||
Message.warning("每个 SKU 至少需要一个单位");
|
||||
return;
|
||||
}
|
||||
const wasBase = sku.value.unitList[index]?.isBase;
|
||||
sku.value.unitList.splice(index, 1);
|
||||
if (wasBase && sku.value.unitList[0]) {
|
||||
sku.value.unitList[0].isBase = true;
|
||||
sku.value.unitList[0].baseQuantityPerUnit = "1";
|
||||
}
|
||||
}
|
||||
|
||||
function setBaseUnit(index: number): void {
|
||||
if (!sku.value) {
|
||||
return;
|
||||
}
|
||||
sku.value.unitList.forEach((unit, unitIndex) => { unit.isBase = unitIndex === index; });
|
||||
const unit = sku.value.unitList[index];
|
||||
if (unit) {
|
||||
unit.baseQuantityPerUnit = "1";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.unit-editor {
|
||||
max-height: 100%;
|
||||
overflow-y: auto;
|
||||
padding: 1rem 0 calc(1rem + env(safe-area-inset-bottom, 0px));
|
||||
|
||||
.editor-context {
|
||||
margin: 0 1rem .75rem;
|
||||
padding: .75rem;
|
||||
background: var(--td-bg-color-secondarycontainer);
|
||||
border-radius: .875rem;
|
||||
|
||||
.context-label,
|
||||
.context-empty {
|
||||
color: var(--td-text-color-secondary);
|
||||
font-size: .75rem;
|
||||
}
|
||||
|
||||
.context-label {
|
||||
display: block;
|
||||
margin-bottom: .25rem;
|
||||
}
|
||||
|
||||
.context-title {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.context-spec-list {
|
||||
gap: .375rem;
|
||||
display: flex;
|
||||
margin-top: .625rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.context-spec {
|
||||
gap: .25rem;
|
||||
display: flex;
|
||||
padding: .25rem .5rem;
|
||||
background: var(--td-bg-color-container);
|
||||
border-radius: .5rem;
|
||||
font-size: .75rem;
|
||||
|
||||
span {
|
||||
color: var(--td-text-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.context-count {
|
||||
margin-top: .625rem;
|
||||
color: var(--td-text-color-secondary);
|
||||
font-size: .75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.unit-list {
|
||||
gap: .75rem;
|
||||
display: flex;
|
||||
padding: 0 1rem 1rem;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.unit-card {
|
||||
border: 1px solid var(--td-component-border);
|
||||
padding: .5rem 0;
|
||||
border-radius: .75rem;
|
||||
}
|
||||
|
||||
.unit-card-header {
|
||||
gap: .5rem;
|
||||
display: flex;
|
||||
padding: 0 .75rem .5rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.unit-form {
|
||||
margin: 0 .75rem;
|
||||
}
|
||||
|
||||
:deep(.t-form-item) {
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.add-button {
|
||||
align-self: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
:global(html.theme-dark) .unit-editor {
|
||||
.editor-context,
|
||||
.context-spec {
|
||||
background: rgba(148, 163, 184, .12);
|
||||
}
|
||||
|
||||
.unit-card {
|
||||
border-color: rgba(148, 163, 184, .3);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -22,12 +22,18 @@
|
||||
<td>
|
||||
<div class="product-name" v-text="item.productName || '未命名产品'" />
|
||||
<div class="product-type" v-text="getProductTypeText(item.productType)" />
|
||||
<div v-if="hasLowStockSku(item)" class="sku-list">
|
||||
<div v-for="sku in getLowStockSkuList(item)" :key="`${sku.skuId}-${sku.skuCode}`" class="sku-item">
|
||||
<span v-text="getSkuName(sku)" />
|
||||
<span v-text="`可用 ${formatQuantity(sku.availableQuantity)}`" />
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="number" v-text="formatQuantity(item.availableQuantity)" />
|
||||
<td class="number" v-text="formatQuantity(item.reservedQuantity)" />
|
||||
<td class="number" v-text="formatQuantity(item.minStockQuantity)" />
|
||||
<td>
|
||||
<t-tag v-if="item.lowStock === 1" theme="danger" variant="light" content="低库存" />
|
||||
<t-tag v-if="hasLowStockSku(item)" theme="danger" variant="light" :content="`低库存 ${formatQuantity(item.lowStockSkuCount)} 个规格`" />
|
||||
<t-tag v-else theme="success" variant="light" content="正常" />
|
||||
</td>
|
||||
</tr>
|
||||
@@ -38,7 +44,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { GaoProductStockHealthView } from "@/types/GaoChart";
|
||||
import type { GaoProductSkuStockHealthView, GaoProductStockHealthView } from "@/types/GaoChart";
|
||||
import { formatQuantity, getNumber } from "@/pages/product/component/chartUtils";
|
||||
|
||||
defineOptions({
|
||||
@@ -47,14 +53,27 @@ defineOptions({
|
||||
|
||||
const props = defineProps<{
|
||||
list: GaoProductStockHealthView[];
|
||||
skuHealthList: GaoProductSkuStockHealthView[];
|
||||
loading: boolean;
|
||||
}>();
|
||||
|
||||
const sortedList = computed(() => [...props.list].sort((left, right) => {
|
||||
const lowStockDiff = getNumber(right.lowStock) - getNumber(left.lowStock);
|
||||
const lowStockDiff = getNumber(right.lowStockSkuCount) - getNumber(left.lowStockSkuCount);
|
||||
return lowStockDiff || getNumber(left.availableQuantity) - getNumber(right.availableQuantity);
|
||||
}));
|
||||
|
||||
function hasLowStockSku(item: GaoProductStockHealthView): boolean {
|
||||
return 0 < getNumber(item.lowStockSkuCount);
|
||||
}
|
||||
|
||||
function getLowStockSkuList(item: GaoProductStockHealthView): GaoProductSkuStockHealthView[] {
|
||||
return props.skuHealthList.filter(sku => sku.productId === item.productId);
|
||||
}
|
||||
|
||||
function getSkuName(item: GaoProductSkuStockHealthView): string {
|
||||
return [item.skuName || "未命名规格", item.specSummary].filter(Boolean).join(" · ");
|
||||
}
|
||||
|
||||
function getProductTypeText(value?: string): string {
|
||||
return ({ SERVICE: "服务", GOODS: "商品", CONSUMABLE: "耗材" } as Record<string, string>)[value || ""] || "未分类";
|
||||
}
|
||||
@@ -127,6 +146,21 @@ function getProductTypeText(value?: string): string {
|
||||
font-size: .6875rem;
|
||||
}
|
||||
|
||||
.sku-list {
|
||||
gap: .25rem;
|
||||
margin-top: .375rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sku-item {
|
||||
gap: .5rem;
|
||||
color: #b42318;
|
||||
display: flex;
|
||||
font-size: .6875rem;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.table-state {
|
||||
padding: 1.5rem 0;
|
||||
display: flex;
|
||||
@@ -148,6 +182,10 @@ function getProductTypeText(value?: string): string {
|
||||
color: #cbd5e1;
|
||||
border-top-color: rgba(148, 163, 184, .2);
|
||||
}
|
||||
|
||||
.sku-item {
|
||||
color: #fda29b;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user