fix ProductStockDetail.vue style
This commit is contained in:
@@ -7,47 +7,62 @@
|
||||
<t-tag :content="getStockStatusText(stock)" variant="light-outline" :theme="getStockTheme(stock)" />
|
||||
</template>
|
||||
</product-profile>
|
||||
<card-container title="库存余额">
|
||||
<t-cell-group theme="card" class="summary">
|
||||
<t-cell title="产品" :note="product.name || '未命名产品'" />
|
||||
<t-cell title="基础单位" :note="getProductBaseUnitName(product) || '-'" />
|
||||
<t-cell title="实际库存" :note="getProductQuantityText(stock.onHandQuantity)" />
|
||||
<t-cell title="预占库存" :note="getProductQuantityText(stock.reservedQuantity)" />
|
||||
<t-cell title="可用库存" :note="getProductQuantityText(stock.availableQuantity)" />
|
||||
</t-cell-group>
|
||||
</card-container>
|
||||
<card-container v-if="canOperate && product.stockEnabled" title="人工库存操作" description="所有操作必须选择业务单位、上传照片并填写原因或备注">
|
||||
<div class="operation-list">
|
||||
<t-button v-for="item in operationList" :key="item.type" :theme="item.theme" variant="outline" @click="openOperation(item.type)">
|
||||
<t-cell-group theme="card" title="库存">
|
||||
<t-cell class="empty-cell-title-margin">
|
||||
<template #title>
|
||||
<span></span>
|
||||
</template>
|
||||
<template #description>
|
||||
<div class="stock">
|
||||
<div class="main">
|
||||
<span class="label">库存</span>
|
||||
<div class="value">
|
||||
<strong class="text" v-text="getProductQuantityText(stock.onHandQuantity)" />
|
||||
<span class="unit" v-text="getProductBaseUnitName(product) || '基础单位'" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<div class="available">
|
||||
<span class="label">可用</span>
|
||||
<strong class="value" v-text="getProductQuantityText(stock.availableQuantity)" />
|
||||
<span class="unit" v-text="getProductBaseUnitName(product) || '基础单位'" />
|
||||
</div>
|
||||
<div class="reserved">
|
||||
<span class="label">预占</span>
|
||||
<strong class="value" v-text="getProductQuantityText(stock.reservedQuantity)" />
|
||||
<span class="unit" v-text="getProductBaseUnitName(product) || '基础单位'" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</t-cell>
|
||||
<t-cell title="查看该产品流水" arrow @click="openRecordList" />
|
||||
</t-cell-group>
|
||||
<card-container
|
||||
v-if="canOperate && product.stockEnabled"
|
||||
class="actions-wrap"
|
||||
title="库存操作"
|
||||
description="所有操作必须选择业务单位、上传照片并填写原因或备注"
|
||||
>
|
||||
<div class="actions">
|
||||
<t-button
|
||||
v-for="item in operationList"
|
||||
:key="item.type"
|
||||
class="action"
|
||||
:theme="item.theme"
|
||||
block
|
||||
@click="openOperation(item.type)"
|
||||
>
|
||||
<template #icon>
|
||||
<t-icon :name="item.icon" size="1.25rem" />
|
||||
</template>
|
||||
<span v-text="item.text" />
|
||||
</t-button>
|
||||
</div>
|
||||
</card-container>
|
||||
<card-container title="库存流水" description="履约产生的预占、释放、出库和冲正流水仅展示,不提供人工修改">
|
||||
<t-cell-group v-if="recordList.length" theme="card" class="records">
|
||||
<t-cell v-for="record in recordList" :key="record.id || `${record.occurredAt}-${record.requestNo}`">
|
||||
<template #title>
|
||||
<div class="record-title">
|
||||
<strong v-text="getStockOperationText(record.operationType)" />
|
||||
<span :class="getDeltaClass(record.onHandDelta)" v-text="getDeltaText(record.onHandDelta)" />
|
||||
</div>
|
||||
</template>
|
||||
<template #note>
|
||||
<span v-text="record.occurredAt ? Time.toDateTime(record.occurredAt) : '时间未知'" />
|
||||
</template>
|
||||
<template #description>
|
||||
<div class="record-description">
|
||||
<span v-text="`${record.quantity || 0} ${record.unitSnapshot || getProductBaseUnitName(product)} · 基础单位 ${record.baseQuantity || 0}`" />
|
||||
<span class="gray" v-text="record.reason || record.remark || '无备注'" />
|
||||
</div>
|
||||
</template>
|
||||
</t-cell>
|
||||
</t-cell-group>
|
||||
<div v-else class="center-tips" v-text="'暂无库存流水'" />
|
||||
</card-container>
|
||||
</template>
|
||||
<t-empty v-else description="库存或产品不存在" />
|
||||
<product-stock-operation-dialog
|
||||
<product-stock-operation-popup
|
||||
v-if="product && stock"
|
||||
v-model:visible="operationVisible"
|
||||
:product="product"
|
||||
@@ -59,50 +74,38 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Time } from "timi-web";
|
||||
import { useUserStore } from "timi-web-api";
|
||||
import ProductAPI from "@/api/ProductAPI";
|
||||
import ProductProfile from "@/pages/product/component/ProductProfile.vue";
|
||||
import ProductStockOperationDialog from "@/pages/product/ProductStockOperationDialog.vue";
|
||||
import ProductStockOperationPopup from "@/pages/product/ProductStockOperationPopup.vue";
|
||||
import { useNavBarStore } from "@/store/navBarStore";
|
||||
import type { Product, ProductStock, StockOperationType, StockRecord } from "@/types/Product";
|
||||
import type { Product, ProductStock, StockOperationType } from "@/types/Product";
|
||||
import { getErrorMessage } from "@/utils/error";
|
||||
import { getProductBaseUnitName, getProductQuantityText, getStockOperationText } from "@/utils/product";
|
||||
import { getProductBaseUnitName, getProductQuantityText } from "@/utils/product";
|
||||
import { Message } from "@/utils/message";
|
||||
import { permissionCodes } from "@/utils/permission";
|
||||
|
||||
type ManualStockOperation = Extract<StockOperationType, "INITIAL_IN" | "MANUAL_IN" | "MANUAL_OUT" | "STOCKTAKE_GAIN" | "STOCKTAKE_LOSS">;
|
||||
type ManualStockOperation = Extract<StockOperationType, "MANUAL_IN" | "MANUAL_OUT" | "STOCKTAKE_GAIN" | "STOCKTAKE_LOSS">;
|
||||
|
||||
defineOptions({ name: "ProductStockDetail" });
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const navBarStore = useNavBarStore();
|
||||
const userStore = useUserStore();
|
||||
const product = ref<Product>();
|
||||
const stock = ref<ProductStock>();
|
||||
const recordList = ref<StockRecord[]>([]);
|
||||
const isLoading = ref(false);
|
||||
const operationVisible = ref(false);
|
||||
const operationType = ref<ManualStockOperation>("MANUAL_IN");
|
||||
const canOperate = computed(() => userStore.hasPermissions(permissionCodes.productStock.create));
|
||||
const operationList: Array<{ text: string; type: ManualStockOperation; theme: "primary" | "danger" }> = [
|
||||
{ text: "期初入库", type: "INITIAL_IN", theme: "primary" },
|
||||
{ text: "手工入库", type: "MANUAL_IN", theme: "primary" },
|
||||
{ text: "手工出库", type: "MANUAL_OUT", theme: "danger" },
|
||||
{ text: "盘盈", type: "STOCKTAKE_GAIN", theme: "primary" },
|
||||
{ text: "盘亏", type: "STOCKTAKE_LOSS", theme: "danger" }
|
||||
const operationList: Array<{ text: string; icon: string; type: ManualStockOperation; theme: "primary" | "danger" }> = [
|
||||
{ text: "手工入库", icon: "import", type: "MANUAL_IN", theme: "primary" },
|
||||
{ text: "手工出库", icon: "export", type: "MANUAL_OUT", theme: "danger" },
|
||||
{ text: "盘盈", icon: "arrow-up", type: "STOCKTAKE_GAIN", theme: "primary" },
|
||||
{ text: "盘亏", icon: "arrow-down", type: "STOCKTAKE_LOSS", theme: "danger" }
|
||||
];
|
||||
|
||||
function getDeltaText(value?: number | string): string {
|
||||
const amount = Number(value || 0);
|
||||
const text = String(value || "0");
|
||||
return `${0 < amount ? "+" : ""}${amount < 0 ? getProductQuantityText(text.slice(1)) : getProductQuantityText(text)}`;
|
||||
}
|
||||
|
||||
function getDeltaClass(value?: number | string): string {
|
||||
return 0 < Number(value || 0) ? "positive" : Number(value || 0) < 0 ? "negative" : "gray";
|
||||
}
|
||||
|
||||
function getStockStatusText(item: ProductStock): string {
|
||||
if (!item.product?.stockEnabled) {
|
||||
return "不管理库存";
|
||||
@@ -128,19 +131,23 @@ async function loadAll(): Promise<void> {
|
||||
}
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const [productResult, stockResult, recordResult] = await Promise.all([
|
||||
const [productResult, stockResult] = await Promise.all([
|
||||
ProductAPI.detail(productId),
|
||||
ProductAPI.stockDetail(productId),
|
||||
ProductAPI.recordList({ productId, index: 0, size: 100 })
|
||||
ProductAPI.stockDetail(productId)
|
||||
]);
|
||||
product.value = productResult;
|
||||
stock.value = { ...stockResult, product: productResult };
|
||||
recordList.value = recordResult.list || [];
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function openRecordList(): Promise<void> {
|
||||
if (product.value?.id) {
|
||||
await router.push({ path: "/management/product-stock-records", query: { productId: product.value.id } });
|
||||
}
|
||||
}
|
||||
|
||||
function openOperation(type: ManualStockOperation): void {
|
||||
operationType.value = type;
|
||||
operationVisible.value = true;
|
||||
@@ -161,47 +168,98 @@ onUnmounted(() => navBarStore.clearRight());
|
||||
<style scoped lang="less">
|
||||
.product-stock-detail {
|
||||
gap: 1rem;
|
||||
display: grid;
|
||||
display: flex;
|
||||
padding: calc(var(--app-nav-height) + 1rem) 0 1rem;
|
||||
flex-direction: column;
|
||||
|
||||
.loading {
|
||||
margin: 3rem auto;
|
||||
}
|
||||
|
||||
.summary,
|
||||
.records {
|
||||
margin: 0;
|
||||
}
|
||||
.stock {
|
||||
|
||||
.operation-list {
|
||||
gap: .75rem;
|
||||
display: grid;
|
||||
padding: 1rem;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
.label {
|
||||
color: var(--td-text-color-secondary);
|
||||
display: block;
|
||||
font-size: .75rem;
|
||||
}
|
||||
|
||||
.t-button {
|
||||
border-radius: .75rem;
|
||||
.value {
|
||||
color: var(--td-brand-color);
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.unit {
|
||||
color: var(--td-text-color-secondary);
|
||||
font-size: .75rem;
|
||||
}
|
||||
|
||||
.main {
|
||||
padding: .75rem;
|
||||
background: var(--td-brand-color-light);
|
||||
border-radius: .875rem;
|
||||
|
||||
.value {
|
||||
gap: .5rem;
|
||||
display: flex;
|
||||
font-size: 2.25rem;
|
||||
margin-top: .25rem;
|
||||
align-items: baseline;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
gap: .75rem;
|
||||
display: grid;
|
||||
margin-top: .75rem;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
|
||||
.available,
|
||||
.reserved {
|
||||
padding: .75rem;
|
||||
background: var(--td-bg-color-secondarycontainer);
|
||||
border-radius: .75rem;
|
||||
}
|
||||
|
||||
.value {
|
||||
display: block;
|
||||
font-size: 1.375rem;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.available {
|
||||
|
||||
.value {
|
||||
color: var(--td-success-color);
|
||||
}
|
||||
}
|
||||
|
||||
.reserved {
|
||||
.value {
|
||||
color: var(--td-warning-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.record-title {
|
||||
gap: .5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.actions-wrap {
|
||||
|
||||
:deep(.content) {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.actions {
|
||||
gap: .75rem;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
|
||||
.action {
|
||||
--td-button-border-radius: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.record-description {
|
||||
gap: .125rem;
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.positive {
|
||||
color: var(--td-success-color);
|
||||
}
|
||||
|
||||
.negative {
|
||||
color: var(--td-error-color);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user