fix ProductStockList.vue style

This commit is contained in:
Timi
2026-09-03 16:45:37 +08:00
parent 8ca2de5676
commit 9b82ec6a04
5 changed files with 200 additions and 185 deletions
+3 -79
View File
@@ -2,42 +2,7 @@
<div v-can="permissionCodes.product.read" class="product-detail">
<t-loading v-if="isLoading" class="loading" size="32px" />
<template v-if="product">
<card-container class="info" v-entrance-transition>
<div class="content">
<div class="left">
<t-image
v-if="getProductPhotoURL(product)"
:src="getProductPhotoURL(product)"
:alt="product.name || '产品图片'"
fit="cover"
class="avatar"
/>
<div
v-else
class="avatar placeholder"
v-text="(product.name || product.code || '产').slice(0, 1)"
/>
</div>
<div class="main">
<div class="header">
<h3 class="name" v-text="product.name || '未命名产品'" />
<div class="code gray" v-text="product.code || '-'" />
</div>
<div class="tags">
<t-tag
variant="light-outline"
theme="primary"
:content="getProductTypeText(product.productType)"
/>
<t-tag
variant="light-outline"
:theme="getProductStatusTheme(product.status)"
:content="getProductStatusText(product.status)"
/>
</div>
</div>
</div>
</card-container>
<product-profile :product="product" />
<t-cell-group title="基本信息" theme="card" v-entrance-transition="70">
<t-cell title="产品分类" :note="product.category?.name || '未分类'" />
<t-cell title="库存管理" :note="product.stockEnabled ? '启用' : '关闭'" />
@@ -114,6 +79,7 @@ import { useUserStore } from "timi-web-api";
import ProductAPI from "@/api/ProductAPI";
import NavBarActionMenu from "@/components/NavBarActionMenu.vue";
import PhotoScroller from "@/components/PhotoScroller.vue";
import ProductProfile from "@/pages/product/component/ProductProfile.vue";
import { useNavBarStore } from "@/store/navBarStore";
import type { NavBarAction } from "@/types/NavBarAction";
import type { PhotoItem } from "@/types/Photo";
@@ -123,10 +89,7 @@ import { getErrorMessage } from "@/utils/error";
import {
formatProductAmount,
getProductBaseUnitName,
getProductPhotoURL,
getProductStatusText,
getProductStatusTheme,
getProductTypeText
getProductStatusText
} from "@/utils/product";
import { Message } from "@/utils/message";
import { permissionCodes } from "@/utils/permission";
@@ -233,45 +196,6 @@ onUnmounted(() => navBarStore.clearRight());
padding: calc(var(--app-nav-height) + 1rem) 0 1rem 0;
flex-direction: column;
.info {
margin: 0 1rem;
.content {
gap: .5rem;
padding: .5rem;
display: flex;
.avatar {
width: 7rem;
height: 7rem;
}
.main {
flex: 1 1 auto;
display: flex;
flex-direction: column;
justify-content: space-between;
.header {
.name {
margin: 0;
}
.code {
font-size: .875rem;
margin-top: .25rem;
}
}
.tags {
gap: .5rem;
display: flex;
}
}
}
}
.units {
.title {
+1 -1
View File
@@ -36,7 +36,7 @@
<div
v-else
class="avatar placeholder"
v-text="(item.name || item.code || '产').slice(0, 1)"
v-text="(item.name || '产').slice(0, 1)"
/>
</template>
<template #title>
+5 -66
View File
@@ -2,17 +2,11 @@
<div v-can="permissionCodes.productStock.read" class="product-stock-detail">
<t-loading v-if="isLoading" class="loading" size="32px" />
<template v-else-if="product && stock">
<section class="hero" v-entrance-transition>
<span class="eyebrow" v-text="'库存中心'" />
<div class="hero-title">
<h1 v-text="product.name || '未命名产品'" />
<product-profile :product="product">
<template #tags>
<t-tag :content="getStockStatusText(stock)" variant="light-outline" :theme="getStockTheme(stock)" />
</div>
<div class="hero-meta">
<span v-text="product.code || '无产品编码'" />
<strong v-text="`${getProductQuantityText(stock.availableQuantity)} ${getProductBaseUnitName(product)}`" />
</div>
</section>
</template>
</product-profile>
<card-container title="库存余额">
<t-cell-group theme="card" class="summary">
<t-cell title="产品" :note="product.name || '未命名产品'" />
@@ -68,6 +62,7 @@
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 { useNavBarStore } from "@/store/navBarStore";
import type { Product, ProductStock, StockOperationType, StockRecord } from "@/types/Product";
@@ -169,51 +164,6 @@ onUnmounted(() => navBarStore.clearRight());
display: grid;
padding: calc(var(--app-nav-height) + 1rem) 0 1rem;
.hero {
margin: 0 1rem;
padding: 1rem;
background: linear-gradient(135deg, #eff6ff, #f0fdf4);
box-shadow: var(--td-shadow-1);
border-radius: 1.25rem;
.eyebrow {
color: #2563eb;
display: block;
font-size: .6875rem;
font-weight: 700;
letter-spacing: .08em;
}
.hero-title {
gap: .5rem;
display: flex;
margin-top: .35rem;
align-items: center;
justify-content: space-between;
}
h1 {
margin: 0;
font-size: 1.25rem;
line-height: 1.35;
}
.hero-meta {
gap: .75rem;
display: flex;
margin-top: .6rem;
color: var(--td-text-color-secondary);
align-items: center;
justify-content: space-between;
}
strong {
color: #0f766e;
font-size: 1.2rem;
white-space: nowrap;
}
}
.loading {
margin: 3rem auto;
}
@@ -254,15 +204,4 @@ onUnmounted(() => navBarStore.clearRight());
}
}
:global(html.theme-dark) .product-stock-detail .hero {
background: linear-gradient(135deg, rgba(37, 99, 235, .2), rgba(15, 118, 110, .18));
.eyebrow {
color: #93c5fd;
}
strong {
color: #99f6e4;
}
}
</style>
+87 -39
View File
@@ -1,24 +1,62 @@
<template>
<div v-can="permissionCodes.productStock.read" class="product-stock-list">
<div class="search bg-white">
<t-input v-model.trim="keyword" class="keyword" type="search" borderless clearable placeholder="搜索产品名称或编码" />
<t-input
v-model="keyword"
class="keyword"
type="search"
borderless
clearable
placeholder="搜索产品名称或编码"
/>
</div>
<t-pull-down-refresh v-model="isRefreshing" :refresh-timeout="30000" @refresh="refresh" @scrolltolower="loadMore">
<t-pull-down-refresh
class="list-wrap empty-cell-title-margin"
v-model="isRefreshing"
:refresh-timeout="30000"
@refresh="refresh"
@scrolltolower="loadMore"
>
<t-cell-group v-if="stockList.length" theme="card" class="list fix-last-cell-line">
<t-cell v-for="(item, index) in stockList" :key="item.productId || item.id" arrow hover class="item" v-entrance-transition="index * 30" @click="toDetail(item)">
<template #leftIcon>
<t-image
v-if="getProductPhotoURL(item.product!)"
:src="getProductPhotoURL(item.product!)"
:alt="item.product?.name || '产品图片'"
fit="cover"
class="avatar"
/>
<div
v-else
class="avatar placeholder"
v-text="(item.product?.name || '产').slice(0, 1)"
/>
</template>
<template #title>
<div class="title">
<strong v-text="item.product?.name || '未命名产品'" />
<t-tag size="small" variant="outline" :theme="getStockTheme(item)" :content="getStockStatusText(item)" />
<t-tag
size="small"
variant="outline"
:content="item.product?.code || '无产品编码'"
/>
</div>
</template>
<template #note>
<span v-text="`${getProductQuantityText(item.availableQuantity)} ${getProductBaseUnitName(item.product)}`" />
</template>
<template #description>
<div class="description">
<span class="gray" v-text="item.product?.code || '无产品编码'" />
<span v-text="`实际 ${getProductQuantityText(item.onHandQuantity)} · 预占 ${getProductQuantityText(item.reservedQuantity)} · 可用 ${getProductQuantityText(item.availableQuantity)}`" />
<div class="stock">
<span class="on-hand">
<span>库存</span>
<strong v-text="getProductQuantityText(item.onHandQuantity)"/>
</span>
<span class="available">
<span>可用</span>
<strong v-text="getProductQuantityText(item.availableQuantity)"/>
</span>
<span class="reserved">
<span>预占</span>
<strong v-text="getProductQuantityText(item.reservedQuantity)" />
</span>
</div>
</template>
</t-cell>
@@ -35,7 +73,7 @@ import ProductAPI from "@/api/ProductAPI";
import { useNavBarStore } from "@/store/navBarStore";
import type { ProductStock } from "@/types/Product";
import { getErrorMessage } from "@/utils/error";
import { getProductBaseUnitName, getProductQuantityText } from "@/utils/product";
import { getProductPhotoURL, getProductQuantityText } from "@/utils/product";
import { Message } from "@/utils/message";
import { permissionCodes } from "@/utils/permission";
@@ -56,24 +94,6 @@ let requestToken = 0;
const emptyText = computed(() => isLoading.value ? "正在加载库存" : keyword.value ? "未找到匹配库存" : "暂无库存记录");
function getStockStatusText(item: ProductStock): string {
if (!item.product?.stockEnabled) {
return "不管理库存";
}
const available = Number(item.availableQuantity || 0);
const min = Number(item.product.minStockQuantity || 0);
return 0 < min && available < min ? "库存不足" : "库存正常";
}
function getStockTheme(item: ProductStock): "danger" | "success" | "default" {
if (!item.product?.stockEnabled) {
return "default";
}
const available = Number(item.availableQuantity || 0);
const min = Number(item.product.minStockQuantity || 0);
return 0 < min && available < min ? "danger" : "success";
}
async function fetch(reset = false): Promise<void> {
if (!reset && (isLoading.value || isNoMore.value)) {
return;
@@ -178,19 +198,47 @@ onUnmounted(() => {
}
}
.list {
padding-top: .75rem;
.list-wrap {
min-height: 70vh;
.item {
.title {
gap: .5rem;
display: flex;
align-items: center;
}
.list {
padding-top: .75rem;
.description {
display: grid;
gap: .125rem;
.item {
padding: 10px;
.avatar {
width: 3.5rem;
height: 3.5rem;
}
.title {
gap: .5rem;
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
}
.stock {
gap: .5rem;
display: flex;
margin-top: .5rem;
flex-wrap: wrap;
strong {
display: inline-block;
margin-left: .25rem;
}
}
.reserved {
color: var(--td-warning-color);
}
.available {
color: var(--td-success-color);
}
}
}
}
@@ -0,0 +1,104 @@
<template>
<card-container class="info" v-entrance-transition>
<div class="content">
<div class="left">
<t-image
v-if="getProductPhotoURL(props.product)"
:src="getProductPhotoURL(props.product)"
:alt="props.product.name || '产品图片'"
fit="cover"
class="avatar"
/>
<div
v-else
class="avatar placeholder"
v-text="(props.product.name || props.product.code || '产').slice(0, 1)"
/>
</div>
<div class="main">
<div class="header">
<h3 class="name" v-text="props.product.name || '未命名产品'" />
<div class="code gray" v-text="props.product.code || '-'" />
</div>
<div class="tags">
<t-tag
variant="light-outline"
theme="primary"
:content="getProductTypeText(props.product.productType)"
/>
<t-tag
variant="light-outline"
:theme="getProductStatusTheme(props.product.status)"
:content="getProductStatusText(props.product.status)"
/>
<slot name="tags" />
</div>
</div>
</div>
</card-container>
</template>
<script setup lang="ts">
import type { Product } from "@/types/Product";
import { getProductPhotoURL, getProductStatusText, getProductStatusTheme, getProductTypeText } from "@/utils/product";
defineOptions({ name: "ProductProfile" });
interface Props {
product: Product;
}
const props = defineProps<Props>();
</script>
<style scoped lang="less">
.info {
margin: 0 1rem;
.content {
gap: .5rem;
padding: .5rem;
display: flex;
.avatar {
width: 7rem;
height: 7rem;
overflow: hidden;
border-radius: .75rem;
}
.placeholder {
color: var(--td-text-color-placeholder);
display: flex;
background: var(--td-bg-color-secondarycontainer);
align-items: center;
justify-content: center;
}
.main {
flex: 1 1 auto;
display: flex;
flex-direction: column;
justify-content: space-between;
.header {
.name {
margin: 0;
}
.code {
font-size: .875rem;
margin-top: .25rem;
}
}
.tags {
gap: .5rem;
display: flex;
flex-wrap: wrap;
}
}
}
}
</style>