feat(product): 新增产品管理模块与字典组件功能
- 新增产品管理相关路由和页面(dashboard、list、requirement、setting) - 实现产品基础信息编辑弹窗组件(base-info-dialog.vue) - 添加运行时字典功能(dict-select、dict-text、dict-tag组件) - 集成字典管理store和API调用 - 规范ID类型定义为string避免精度丢失问题 - 完善国际化资源文件支持中英文对照 - 新增对象上下文业务域入口页导航实现说明 - 添加Vue DevTools浮动入口注释说明 - 统一权限控制支持全局和对象作用域区分 - 规范分页查询参数类型定义与使用方式
This commit is contained in:
696
src/views/product/list/index.vue
Normal file
696
src/views/product/list/index.vue
Normal file
@@ -0,0 +1,696 @@
|
||||
<script setup lang="tsx">
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import type { Component } from 'vue';
|
||||
import { ElButton, ElTag } from 'element-plus';
|
||||
import dayjs from 'dayjs';
|
||||
import { CircleCheckFilled, DeleteFilled, FolderOpened, VideoPause } from '@element-plus/icons-vue';
|
||||
import { RDMS_OBJECT_DIRECTION_DICT_CODE } from '@/constants/dict';
|
||||
import { OBJECT_CONTEXT_QUERY_KEY } from '@/constants/object-context';
|
||||
import { fetchGetProductPage, fetchGetUserSimpleList } from '@/service/api';
|
||||
import { useDict } from '@/hooks/business/dict';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { useUIPaginatedTable } from '@/hooks/common/table';
|
||||
import BusinessTableActionCell from '@/components/custom/business-table-action-cell';
|
||||
import { getProductStatusLabel, getProductStatusTagType, isProductEditable } from '../shared/product-master-data';
|
||||
import ProductOperateDialog from './modules/product-operate-dialog.vue';
|
||||
import ProductSearch from './modules/product-search.vue';
|
||||
|
||||
defineOptions({ name: 'ProductList' });
|
||||
|
||||
interface StatusNavMeta {
|
||||
key: Api.Product.ProductStatusCode;
|
||||
label: string;
|
||||
description: string;
|
||||
tone: 'teal' | 'slate' | 'amber' | 'rose';
|
||||
icon: Component;
|
||||
}
|
||||
|
||||
type ProductPageResponse = Awaited<ReturnType<typeof fetchGetProductPage>>;
|
||||
|
||||
const PRODUCT_OPTION_PAGE_SIZE = 200;
|
||||
const PRODUCT_ENTRY_ROUTE_PATH = '/product/list';
|
||||
|
||||
function getInitSearchParams(): Api.Product.ProductSearchParams {
|
||||
return {
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
keyword: '',
|
||||
directionCode: undefined,
|
||||
managerUserId: undefined,
|
||||
statusCode: undefined,
|
||||
updateTime: undefined
|
||||
};
|
||||
}
|
||||
|
||||
function transformProductPage(response: ProductPageResponse, pageNo: number, pageSize: number) {
|
||||
if (!response.error && response.data) {
|
||||
return {
|
||||
data: response.data.list,
|
||||
pageNum: pageNo,
|
||||
pageSize,
|
||||
total: response.data.total
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
data: [],
|
||||
pageNum: pageNo,
|
||||
pageSize,
|
||||
total: 0
|
||||
};
|
||||
}
|
||||
|
||||
function sortManagerOptions(list: Api.SystemManage.UserSimple[]) {
|
||||
return list.slice().sort((left, right) => left.nickname.localeCompare(right.nickname, 'zh-CN'));
|
||||
}
|
||||
|
||||
function formatDateTime(value?: string | null) {
|
||||
if (!value) {
|
||||
return '--';
|
||||
}
|
||||
|
||||
return dayjs(value).format('YYYY-MM-DD HH:mm:ss');
|
||||
}
|
||||
|
||||
async function fetchProductTotal(params: Api.Product.ProductSearchParams) {
|
||||
const { error, data } = await fetchGetProductPage({
|
||||
...params,
|
||||
pageNo: 1,
|
||||
pageSize: 1
|
||||
});
|
||||
|
||||
if (error || !data) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return data.total;
|
||||
}
|
||||
|
||||
async function fetchAllProducts() {
|
||||
async function collect(pageNo: number, list: Api.Product.Product[]): Promise<Api.Product.Product[] | null> {
|
||||
const { error, data } = await fetchGetProductPage({
|
||||
pageNo,
|
||||
pageSize: PRODUCT_OPTION_PAGE_SIZE
|
||||
});
|
||||
|
||||
if (error || !data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nextList = list.concat(data.list);
|
||||
|
||||
if (nextList.length >= data.total || data.list.length === 0) {
|
||||
return nextList;
|
||||
}
|
||||
|
||||
return collect(pageNo + 1, nextList);
|
||||
}
|
||||
|
||||
return collect(1, []);
|
||||
}
|
||||
|
||||
function createManagerOptions(products: Api.Product.Product[], users: Api.SystemManage.UserSimple[]) {
|
||||
const managerIdSet = new Set(products.map(item => String(item.managerUserId)).filter(Boolean));
|
||||
const userMap = new Map(users.map(item => [String(item.id), item]));
|
||||
|
||||
const options = Array.from(managerIdSet).map(managerUserId => {
|
||||
return (
|
||||
userMap.get(managerUserId) || {
|
||||
id: managerUserId,
|
||||
nickname: String(managerUserId)
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
return sortManagerOptions(options);
|
||||
}
|
||||
|
||||
const statusNavMetas: StatusNavMeta[] = [
|
||||
{
|
||||
key: 'active',
|
||||
label: '启用产品',
|
||||
description: '当前正常服务中的产品',
|
||||
tone: 'teal',
|
||||
icon: CircleCheckFilled
|
||||
},
|
||||
{
|
||||
key: 'archived',
|
||||
label: '归档产品',
|
||||
description: '已完成阶段目标的产品',
|
||||
tone: 'slate',
|
||||
icon: FolderOpened
|
||||
},
|
||||
{
|
||||
key: 'paused',
|
||||
label: '暂停产品',
|
||||
description: '阶段性暂停投入的产品',
|
||||
tone: 'amber',
|
||||
icon: VideoPause
|
||||
},
|
||||
{
|
||||
key: 'abandoned',
|
||||
label: '废弃产品',
|
||||
description: '已明确停止建设的产品',
|
||||
tone: 'rose',
|
||||
icon: DeleteFilled
|
||||
}
|
||||
];
|
||||
|
||||
const searchParams = reactive(getInitSearchParams());
|
||||
const selectedStatus = ref<Api.Product.ProductStatusCode>('active');
|
||||
const managerFilterOptions = ref<Api.SystemManage.UserSimple[]>([]);
|
||||
const managerUserOptions = ref<Api.SystemManage.UserSimple[]>([]);
|
||||
const operateVisible = ref(false);
|
||||
const editingRow = ref<Api.Product.Product | null>(null);
|
||||
const { routerPush } = useRouterPush();
|
||||
|
||||
const { dictData: directionOptions, getLabel: getDirectionDictLabel } = useDict(RDMS_OBJECT_DIRECTION_DICT_CODE);
|
||||
|
||||
const statusCounts = ref<Record<Api.Product.ProductStatusCode, number>>({
|
||||
active: 0,
|
||||
archived: 0,
|
||||
paused: 0,
|
||||
abandoned: 0
|
||||
});
|
||||
|
||||
const recentUpdatedCount = ref(0);
|
||||
|
||||
const managerLabelMap = computed(() => {
|
||||
return new Map(managerUserOptions.value.map(item => [String(item.id), item.nickname]));
|
||||
});
|
||||
|
||||
const statusItems = computed(() =>
|
||||
statusNavMetas.map(item => ({
|
||||
...item,
|
||||
count: statusCounts.value[item.key]
|
||||
}))
|
||||
);
|
||||
|
||||
const overviewMetrics = computed(() => [
|
||||
{
|
||||
label: '可见产品',
|
||||
value: Object.values(statusCounts.value).reduce((sum, count) => sum + count, 0),
|
||||
hint: '当前接口可查询到的产品总量'
|
||||
},
|
||||
{
|
||||
label: '当前启用',
|
||||
value: statusCounts.value.active,
|
||||
hint: '正在持续服务和维护的产品'
|
||||
},
|
||||
{
|
||||
label: '产品方向',
|
||||
value: directionOptions.value.length,
|
||||
hint: '已加载的方向字典项数量'
|
||||
},
|
||||
{
|
||||
label: '30天内更新',
|
||||
value: recentUpdatedCount.value,
|
||||
hint: '最近 30 天内发生过更新的产品'
|
||||
}
|
||||
]);
|
||||
|
||||
function getDirectionLabel(directionCode?: string | null) {
|
||||
return getDirectionDictLabel(directionCode, '--');
|
||||
}
|
||||
|
||||
function getManagerLabel(managerUserId?: string | null) {
|
||||
if (!managerUserId) {
|
||||
return '--';
|
||||
}
|
||||
|
||||
return managerLabelMap.value.get(String(managerUserId)) || String(managerUserId);
|
||||
}
|
||||
|
||||
function createRequestParams(): Api.Product.ProductSearchParams {
|
||||
return {
|
||||
...searchParams,
|
||||
keyword: searchParams.keyword?.trim() || undefined,
|
||||
statusCode: selectedStatus.value
|
||||
};
|
||||
}
|
||||
|
||||
const { columns, columnChecks, data, loading, getDataByPage, mobilePagination } = useUIPaginatedTable<
|
||||
ProductPageResponse,
|
||||
Api.Product.Product
|
||||
>({
|
||||
paginationProps: {
|
||||
currentPage: searchParams.pageNo,
|
||||
pageSize: searchParams.pageSize
|
||||
},
|
||||
api: () => fetchGetProductPage(createRequestParams()),
|
||||
transform: response => transformProductPage(response, searchParams.pageNo ?? 1, searchParams.pageSize ?? 10),
|
||||
onPaginationParamsChange: params => {
|
||||
searchParams.pageNo = params.currentPage ?? 1;
|
||||
searchParams.pageSize = params.pageSize ?? 10;
|
||||
},
|
||||
columns: () => [
|
||||
{ prop: 'index', type: 'index', label: '序号', width: 64 },
|
||||
{
|
||||
prop: 'name',
|
||||
label: '产品名称',
|
||||
minWidth: 220,
|
||||
formatter: row => (
|
||||
<ElButton link type="primary" class="product-name-link" onClick={() => enterProductContext(row)}>
|
||||
{row.name}
|
||||
</ElButton>
|
||||
)
|
||||
},
|
||||
{ prop: 'code', label: '产品编码', minWidth: 140, showOverflowTooltip: true },
|
||||
{
|
||||
prop: 'managerUserId',
|
||||
label: '产品经理',
|
||||
minWidth: 120,
|
||||
formatter: row => getManagerLabel(row.managerUserId)
|
||||
},
|
||||
{
|
||||
prop: 'directionCode',
|
||||
label: '产品方向',
|
||||
minWidth: 140,
|
||||
showOverflowTooltip: true,
|
||||
formatter: row => getDirectionLabel(row.directionCode)
|
||||
},
|
||||
{
|
||||
prop: 'statusCode',
|
||||
label: '管理状态',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
formatter: row => (
|
||||
<ElTag type={getProductStatusTagType(row.statusCode)}>{getProductStatusLabel(row.statusCode)}</ElTag>
|
||||
)
|
||||
},
|
||||
{
|
||||
prop: 'lastStatusReason',
|
||||
label: '状态原因',
|
||||
minWidth: 180,
|
||||
showOverflowTooltip: true,
|
||||
formatter: row => row.lastStatusReason?.trim() || '--'
|
||||
},
|
||||
{
|
||||
prop: 'updateTime',
|
||||
label: '最近更新',
|
||||
width: 170,
|
||||
align: 'center',
|
||||
formatter: row => formatDateTime(row.updateTime)
|
||||
},
|
||||
{
|
||||
prop: 'operate',
|
||||
label: '操作',
|
||||
width: 108,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
formatter: row => (
|
||||
<BusinessTableActionCell
|
||||
actions={[
|
||||
{
|
||||
key: 'edit',
|
||||
label: '编辑',
|
||||
buttonType: 'primary',
|
||||
disabled: !isProductEditable(row.statusCode),
|
||||
onClick: () => openEdit(row)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
async function loadManagerOptions() {
|
||||
const [allProducts, userSimpleResult] = await Promise.all([fetchAllProducts(), fetchGetUserSimpleList()]);
|
||||
|
||||
const userSimpleList =
|
||||
userSimpleResult.error || !userSimpleResult.data ? [] : sortManagerOptions(userSimpleResult.data);
|
||||
|
||||
managerUserOptions.value = userSimpleList;
|
||||
|
||||
if (!allProducts) {
|
||||
managerFilterOptions.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
managerFilterOptions.value = createManagerOptions(allProducts, userSimpleList);
|
||||
}
|
||||
|
||||
async function loadOverviewData() {
|
||||
const end = dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss');
|
||||
const start = dayjs().subtract(30, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss');
|
||||
|
||||
const [activeTotal, archivedTotal, pausedTotal, abandonedTotal, recentTotal] = await Promise.all([
|
||||
fetchProductTotal({ pageNo: 1, pageSize: 1, statusCode: 'active' }),
|
||||
fetchProductTotal({ pageNo: 1, pageSize: 1, statusCode: 'archived' }),
|
||||
fetchProductTotal({ pageNo: 1, pageSize: 1, statusCode: 'paused' }),
|
||||
fetchProductTotal({ pageNo: 1, pageSize: 1, statusCode: 'abandoned' }),
|
||||
fetchProductTotal({ pageNo: 1, pageSize: 1, updateTime: [start, end] })
|
||||
]);
|
||||
|
||||
statusCounts.value = {
|
||||
active: activeTotal,
|
||||
archived: archivedTotal,
|
||||
paused: pausedTotal,
|
||||
abandoned: abandonedTotal
|
||||
};
|
||||
recentUpdatedCount.value = recentTotal;
|
||||
}
|
||||
|
||||
async function reloadProductTable(page = searchParams.pageNo ?? 1) {
|
||||
await getDataByPage(page);
|
||||
}
|
||||
|
||||
async function refreshPageData(page = searchParams.pageNo ?? 1) {
|
||||
await Promise.all([loadManagerOptions(), loadOverviewData(), reloadProductTable(page)]);
|
||||
}
|
||||
|
||||
async function handleSearch() {
|
||||
await reloadProductTable(1);
|
||||
}
|
||||
|
||||
async function handleResetSearch() {
|
||||
const pageSize = searchParams.pageSize ?? 10;
|
||||
|
||||
Object.assign(searchParams, getInitSearchParams(), {
|
||||
pageSize
|
||||
});
|
||||
|
||||
await reloadProductTable(1);
|
||||
}
|
||||
|
||||
async function handleStatusChange(status: Api.Product.ProductStatusCode) {
|
||||
selectedStatus.value = status;
|
||||
await reloadProductTable(1);
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
editingRow.value = null;
|
||||
operateVisible.value = true;
|
||||
}
|
||||
|
||||
function openEdit(row: Api.Product.Product) {
|
||||
editingRow.value = row;
|
||||
operateVisible.value = true;
|
||||
}
|
||||
|
||||
async function enterProductContext(row: Api.Product.Product) {
|
||||
await routerPush({
|
||||
path: PRODUCT_ENTRY_ROUTE_PATH,
|
||||
query: {
|
||||
[OBJECT_CONTEXT_QUERY_KEY]: row.id
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function handleProductSubmitted(productId?: string) {
|
||||
const isEditing = Boolean(productId && editingRow.value?.id === productId);
|
||||
|
||||
await refreshPageData(isEditing ? (searchParams.pageNo ?? 1) : 1);
|
||||
|
||||
if (isEditing) {
|
||||
editingRow.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await refreshPageData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="min-h-560px gap-16px overflow-hidden xl:grid xl:grid-cols-[396px_minmax(0,1fr)] lt-xl:flex lt-xl:flex-col lt-xl:overflow-auto"
|
||||
>
|
||||
<div class="flex-col-stretch gap-16px xl:min-h-0">
|
||||
<ElCard class="product-overview-card card-wrapper">
|
||||
<div class="product-overview-card__stats">
|
||||
<div v-for="item in overviewMetrics" :key="item.label" class="product-overview-card__stat">
|
||||
<span class="product-overview-card__stat-label">{{ item.label }}</span>
|
||||
<strong class="product-overview-card__stat-value">{{ item.value }}</strong>
|
||||
<small class="product-overview-card__stat-hint">{{ item.hint }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="product-status-panel__list">
|
||||
<button
|
||||
v-for="item in statusItems"
|
||||
:key="item.key"
|
||||
type="button"
|
||||
class="product-status-item"
|
||||
:class="[`product-status-item--${item.tone}`, { 'is-active': selectedStatus === item.key }]"
|
||||
:aria-pressed="selectedStatus === item.key"
|
||||
@click="handleStatusChange(item.key)"
|
||||
>
|
||||
<div class="product-status-item__icon">
|
||||
<ElIcon>
|
||||
<component :is="item.icon" />
|
||||
</ElIcon>
|
||||
</div>
|
||||
<div class="product-status-item__main">
|
||||
<div class="product-status-item__top">
|
||||
<strong>{{ item.label }}</strong>
|
||||
<em>{{ item.count }}</em>
|
||||
</div>
|
||||
<p class="product-status-item__desc">{{ item.description }}</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</ElCard>
|
||||
</div>
|
||||
|
||||
<div class="flex-col-stretch gap-16px xl:min-h-0">
|
||||
<ProductSearch
|
||||
v-model:model="searchParams"
|
||||
:manager-options="managerFilterOptions"
|
||||
@reset="handleResetSearch"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
|
||||
<ElCard class="card-wrapper xl:flex-1-hidden" body-class="product-table-card-body">
|
||||
<template #header>
|
||||
<div class="product-card-header">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex flex-wrap items-center gap-8px">
|
||||
<p class="truncate text-16px font-600">产品列表</p>
|
||||
<ElTag effect="plain" :type="getProductStatusTagType(selectedStatus)">
|
||||
{{
|
||||
statusItems.find(item => item.key === selectedStatus)?.label ||
|
||||
getProductStatusLabel(selectedStatus)
|
||||
}}
|
||||
</ElTag>
|
||||
<ElTag effect="plain">{{ mobilePagination.total || data.length }}</ElTag>
|
||||
</div>
|
||||
</div>
|
||||
<TableHeaderOperation
|
||||
v-model:columns="columnChecks"
|
||||
:disabled-delete="true"
|
||||
:loading="loading"
|
||||
@refresh="refreshPageData"
|
||||
>
|
||||
<template #default>
|
||||
<ElButton plain type="primary" @click="openCreate">
|
||||
<template #icon>
|
||||
<icon-ic-round-plus class="text-icon" />
|
||||
</template>
|
||||
新增
|
||||
</ElButton>
|
||||
</template>
|
||||
</TableHeaderOperation>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="flex-1">
|
||||
<ElTable v-loading="loading" height="100%" border row-key="id" :data="data">
|
||||
<ElTableColumn v-for="col in columns" :key="String(col.prop)" v-bind="col" />
|
||||
|
||||
<template #empty>
|
||||
<ElEmpty description="当前筛选条件下暂无产品" />
|
||||
</template>
|
||||
</ElTable>
|
||||
</div>
|
||||
<div class="mt-20px flex justify-end">
|
||||
<ElPagination
|
||||
v-if="mobilePagination.total"
|
||||
layout="total,prev,pager,next,sizes"
|
||||
v-bind="mobilePagination"
|
||||
@current-change="mobilePagination['current-change']"
|
||||
@size-change="mobilePagination['size-change']"
|
||||
/>
|
||||
</div>
|
||||
</ElCard>
|
||||
</div>
|
||||
|
||||
<ProductOperateDialog
|
||||
v-model:visible="operateVisible"
|
||||
:manager-user-options="managerUserOptions"
|
||||
:row-data="editingRow"
|
||||
@submitted="handleProductSubmitted"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.product-overview-card {
|
||||
overflow: hidden;
|
||||
border: 1px solid rgb(226 232 240 / 92%);
|
||||
background:
|
||||
radial-gradient(circle at top left, rgb(15 118 110 / 8%), transparent 36%),
|
||||
linear-gradient(180deg, rgb(255 255 255 / 99%), rgb(248 250 252 / 97%));
|
||||
}
|
||||
|
||||
.product-overview-card__stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.product-overview-card__stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 14px 16px;
|
||||
border: 1px solid rgb(226 232 240 / 88%);
|
||||
border-radius: 18px;
|
||||
background-color: rgb(255 255 255 / 84%);
|
||||
}
|
||||
|
||||
.product-overview-card__stat-label {
|
||||
color: rgb(100 116 139 / 90%);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.product-overview-card__stat-value {
|
||||
color: rgb(15 23 42 / 94%);
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.product-overview-card__stat-hint {
|
||||
color: rgb(100 116 139 / 90%);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.product-status-panel__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.product-status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
border: 1px solid rgb(226 232 240 / 90%);
|
||||
border-radius: 18px;
|
||||
background-color: rgb(255 255 255 / 86%);
|
||||
text-align: left;
|
||||
transition:
|
||||
transform 0.2s ease,
|
||||
border-color 0.2s ease,
|
||||
box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.product-status-item:hover {
|
||||
transform: translateY(-1px);
|
||||
border-color: rgb(148 163 184 / 60%);
|
||||
}
|
||||
|
||||
.product-status-item.is-active {
|
||||
border-color: rgb(15 118 110 / 40%);
|
||||
box-shadow: 0 10px 24px rgb(15 118 110 / 8%);
|
||||
}
|
||||
|
||||
.product-status-item__icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 14px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.product-status-item__main {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.product-status-item__top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.product-status-item__top strong {
|
||||
color: rgb(15 23 42 / 94%);
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.product-status-item__top em {
|
||||
color: rgb(15 23 42 / 88%);
|
||||
font-size: 18px;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.product-status-item__desc {
|
||||
color: rgb(100 116 139 / 94%);
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.product-status-item--teal .product-status-item__icon {
|
||||
background-color: rgb(240 253 250 / 96%);
|
||||
color: rgb(15 118 110 / 96%);
|
||||
}
|
||||
|
||||
.product-status-item--slate .product-status-item__icon {
|
||||
background-color: rgb(241 245 249 / 96%);
|
||||
color: rgb(51 65 85 / 92%);
|
||||
}
|
||||
|
||||
.product-status-item--amber .product-status-item__icon {
|
||||
background-color: rgb(255 251 235 / 96%);
|
||||
color: rgb(217 119 6 / 92%);
|
||||
}
|
||||
|
||||
.product-status-item--rose .product-status-item__icon {
|
||||
background-color: rgb(255 241 242 / 96%);
|
||||
color: rgb(225 29 72 / 92%);
|
||||
}
|
||||
|
||||
:deep(.product-table-card-body) {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.product-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.product-name-link {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@media (width <= 1280px) {
|
||||
.product-card-header,
|
||||
.product-status-item__top {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 640px) {
|
||||
.product-overview-card__stats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user