Compare commits
3 Commits
238f7cfcf9
...
7356acec89
| Author | SHA1 | Date | |
|---|---|---|---|
| 7356acec89 | |||
| f114bc683e | |||
| 2fff1b2c43 |
157
backend/scripts/cleanup_categories.py
Normal file
157
backend/scripts/cleanup_categories.py
Normal file
@ -0,0 +1,157 @@
|
||||
"""清理多余产品分类脚本。
|
||||
|
||||
将数据库中多余的分类迁移到工业品(1)或日用品(2),然后软删除多余分类。
|
||||
|
||||
运行方式: python -m backend.scripts.cleanup_categories
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from sqlalchemy import select, func
|
||||
|
||||
from backend.app.db import SessionLocal
|
||||
from backend.app.models.business import Product, ProductCategory
|
||||
|
||||
# 保留的分类 ID
|
||||
KEEP_IDS = {1, 2}
|
||||
|
||||
# 工业关键词:产品名包含这些词的归入工业品
|
||||
INDUSTRY_KEYWORDS = ["布", "带", "胶", "板", "管", "钢", "铁", "铝", "铜", "塑", "膜", "网", "绳", "线", "丝", "棉", "纱"]
|
||||
|
||||
|
||||
def classify_product(product_name: str) -> int:
|
||||
"""根据产品名判断归入工业品(1)还是日用品(2)"""
|
||||
for kw in INDUSTRY_KEYWORDS:
|
||||
if kw in product_name:
|
||||
return 1
|
||||
return 2
|
||||
|
||||
|
||||
def main():
|
||||
session = SessionLocal()
|
||||
try:
|
||||
# ========== Step 1: 查询当前状态 ==========
|
||||
print("=" * 60)
|
||||
print("产品分类清理脚本")
|
||||
print("=" * 60)
|
||||
|
||||
categories = session.execute(
|
||||
select(ProductCategory).where(ProductCategory.deleted == 0).order_by(ProductCategory.id)
|
||||
).scalars().all()
|
||||
|
||||
print(f"\n当前分类列表(共 {len(categories)} 个):")
|
||||
print("-" * 50)
|
||||
|
||||
extra_categories = []
|
||||
for cat in categories:
|
||||
product_count = session.execute(
|
||||
select(func.count(Product.id)).where(
|
||||
Product.category_id == cat.id,
|
||||
Product.deleted == 0,
|
||||
)
|
||||
).scalar() or 0
|
||||
|
||||
tag = "✓ 保留" if cat.id in KEEP_IDS else "✗ 待删除"
|
||||
print(f" ID={cat.id} {cat.category_name:<12} ({cat.category_code}) 产品数: {product_count} [{tag}]")
|
||||
|
||||
if cat.id not in KEEP_IDS:
|
||||
extra_categories.append((cat, product_count))
|
||||
|
||||
if not extra_categories:
|
||||
print("\n没有需要清理的多余分类。")
|
||||
return
|
||||
|
||||
# 统计需要迁移的产品
|
||||
total_products_to_migrate = sum(count for _, count in extra_categories)
|
||||
print(f"\n需要迁移的产品总数: {total_products_to_migrate}")
|
||||
print(f"需要删除的分类数: {len(extra_categories)}")
|
||||
|
||||
# 显示迁移计划
|
||||
print("\n迁移计划:")
|
||||
print("-" * 50)
|
||||
for cat, count in extra_categories:
|
||||
products = session.execute(
|
||||
select(Product).where(
|
||||
Product.category_id == cat.id,
|
||||
Product.deleted == 0,
|
||||
)
|
||||
).scalars().all()
|
||||
|
||||
industry_count = sum(1 for p in products if classify_product(p.product_name) == 1)
|
||||
daily_count = count - industry_count
|
||||
|
||||
print(f" 分类「{cat.category_name}」({count}个产品):")
|
||||
print(f" → 工业品: {industry_count} 个")
|
||||
print(f" → 日用品: {daily_count} 个")
|
||||
|
||||
for p in products:
|
||||
target = "工业品" if classify_product(p.product_name) == 1 else "日用品"
|
||||
print(f" - {p.product_name} ({p.specification}) → {target}")
|
||||
|
||||
# ========== Step 2: 确认 ==========
|
||||
print("\n" + "=" * 60)
|
||||
confirm = input("确认执行清理?(输入 yes 继续): ").strip()
|
||||
if confirm.lower() != "yes":
|
||||
print("已取消。")
|
||||
return
|
||||
|
||||
# ========== Step 3: 执行迁移 ==========
|
||||
print("\n执行迁移...")
|
||||
migrated = 0
|
||||
for cat, count in extra_categories:
|
||||
products = session.execute(
|
||||
select(Product).where(
|
||||
Product.category_id == cat.id,
|
||||
Product.deleted == 0,
|
||||
)
|
||||
).scalars().all()
|
||||
|
||||
for p in products:
|
||||
target_id = classify_product(p.product_name)
|
||||
target_name = "工业品" if target_id == 1 else "日用品"
|
||||
p.category_id = target_id
|
||||
p.category = target_name
|
||||
migrated += 1
|
||||
print(f" 迁移: {p.product_name} → {target_name}")
|
||||
|
||||
# ========== Step 4: 软删除多余分类 ==========
|
||||
print("\n删除多余分类...")
|
||||
deleted = 0
|
||||
for cat, _ in extra_categories:
|
||||
cat.deleted = 1
|
||||
deleted += 1
|
||||
print(f" 删除: {cat.category_name}")
|
||||
|
||||
session.commit()
|
||||
|
||||
# ========== Step 5: 验证 ==========
|
||||
print("\n" + "=" * 60)
|
||||
print("清理完成!验证结果:")
|
||||
|
||||
remaining = session.execute(
|
||||
select(ProductCategory).where(ProductCategory.deleted == 0).order_by(ProductCategory.id)
|
||||
).scalars().all()
|
||||
|
||||
print(f"剩余分类数: {len(remaining)}")
|
||||
for cat in remaining:
|
||||
product_count = session.execute(
|
||||
select(func.count(Product.id)).where(
|
||||
Product.category_id == cat.id,
|
||||
Product.deleted == 0,
|
||||
)
|
||||
).scalar() or 0
|
||||
print(f" ID={cat.id} {cat.category_name} 产品数: {product_count}")
|
||||
|
||||
print(f"\n共迁移 {migrated} 个产品,删除 {deleted} 个分类。")
|
||||
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -730,6 +730,7 @@ export async function fetchProductList(filters = {}) {
|
||||
unit: spec.unit || "-",
|
||||
costPrice: Number(spec.cost_price || 0).toFixed(2),
|
||||
salePrice: Number(spec.sale_price || 0).toFixed(2),
|
||||
isDefault: Boolean(spec.is_default),
|
||||
status: Number(spec.status || 1) === 1 ? "启用" : "停用",
|
||||
remark: spec.remark || "-",
|
||||
})),
|
||||
|
||||
@ -535,27 +535,70 @@
|
||||
<tr>
|
||||
<th><input type="checkbox" :checked="selectedProductIds.length === productRows.length && productRows.length > 0" @change="toggleAllProducts($event)" /></th>
|
||||
<th>产品名称</th>
|
||||
<th>规格数量</th>
|
||||
<th>分类</th>
|
||||
<th>规格数</th>
|
||||
<th>价格范围</th>
|
||||
<th>状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in productRows" :key="row.productName">
|
||||
<td><input type="checkbox" :value="row.productId" v-model="selectedProductIds" /></td>
|
||||
<td>{{ row.productName }}</td>
|
||||
<td>{{ row.specifications.length }}</td>
|
||||
<td>{{ row.categoryName }}</td>
|
||||
<td>{{ row.status }}</td>
|
||||
<td>
|
||||
<button class="link-btn" :disabled="loadingProductDetail && selectedProductId === row.productName" @click="handleViewProductDetail(row)">
|
||||
{{ loadingProductDetail && selectedProductId === row.productName ? "加载中..." : "查看详情" }}
|
||||
</button>
|
||||
<button class="link-btn danger-link" @click="handleDeleteProduct(row)">删除</button>
|
||||
<button class="link-btn danger-link" @click="handlePermanentDeleteProduct(row)">永久删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
<template v-for="row in productRows" :key="row.productName">
|
||||
<tr class="product-row" :class="{ expanded: expandedProducts.has(row.productName) }">
|
||||
<td><input type="checkbox" :value="row.productId" v-model="selectedProductIds" /></td>
|
||||
<td class="product-name-cell" @click="toggleExpand(row.productName)">
|
||||
<span class="expand-icon">{{ expandedProducts.has(row.productName) ? '▼' : '▶' }}</span>
|
||||
{{ row.productName }}
|
||||
</td>
|
||||
<td>{{ row.categoryName }}</td>
|
||||
<td>{{ row.specifications.length }}</td>
|
||||
<td>{{ priceRange(row) }}</td>
|
||||
<td>{{ row.status }}</td>
|
||||
<td>
|
||||
<button class="link-btn" @click="toggleExpand(row.productName)">
|
||||
{{ expandedProducts.has(row.productName) ? '收起' : '展开' }}
|
||||
</button>
|
||||
<button class="link-btn danger-link" @click="handleDeleteProduct(row)">删除</button>
|
||||
<button class="link-btn danger-link" @click="handlePermanentDeleteProduct(row)">永久删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="expandedProducts.has(row.productName)" class="spec-sub-row">
|
||||
<td colspan="7">
|
||||
<div class="spec-sub-table">
|
||||
<table class="inner-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>规格名称</th>
|
||||
<th>单位</th>
|
||||
<th>成本价</th>
|
||||
<th>售价</th>
|
||||
<th>状态</th>
|
||||
<th>默认</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="spec in row.specifications" :key="spec.productId">
|
||||
<td>{{ spec.specification }}</td>
|
||||
<td>{{ spec.unit }}</td>
|
||||
<td>¥{{ spec.costPrice }}</td>
|
||||
<td>¥{{ spec.salePrice }}</td>
|
||||
<td>{{ spec.status }}</td>
|
||||
<td>
|
||||
<span v-if="spec.isDefault" class="default-badge">✓ 默认</span>
|
||||
<button v-else class="link-btn" @click="handleSetDefaultSpec(spec)">设为默认</button>
|
||||
</td>
|
||||
<td>
|
||||
<button class="link-btn" @click="startEditSpecFromRow(spec, row)">编辑</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="spec-sub-hint">新增规格请通过"新增产品"按钮添加。</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
<div v-if="selectedProductIds.length" style="margin-top:8px;">
|
||||
@ -563,67 +606,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="productDetail" class="modal-mask" @click.self="closeProductDetail">
|
||||
<section class="modal-panel detail-modal wide-detail-modal">
|
||||
<div class="modal-header">
|
||||
<div>
|
||||
<h4>产品详情</h4>
|
||||
<span>产品主档:{{ productDetail.product_name }}</span>
|
||||
</div>
|
||||
<button class="modal-close" type="button" @click="closeProductDetail">×</button>
|
||||
</div>
|
||||
<div class="detail-grid product-master-grid">
|
||||
<article>
|
||||
<h5>主档信息</h5>
|
||||
<p>分类:{{ productDetail.category_name || "-" }}</p>
|
||||
<p>状态:{{ Number(productDetail.status || 1) === 1 ? "启用" : "停用" }}</p>
|
||||
<p>备注:{{ productDetail.remark || "-" }}</p>
|
||||
</article>
|
||||
<article>
|
||||
<h5>规格总览</h5>
|
||||
<p>规格数量:{{ productDetail.specifications.length }}</p>
|
||||
<p>默认规格:{{ productDetail.specifications.find((item) => item.is_default)?.specification || "-" }}</p>
|
||||
<p>支持展开后逐个编辑规格明细。</p>
|
||||
</article>
|
||||
</div>
|
||||
<div class="spec-list">
|
||||
<details v-for="spec in productDetail.specifications" :key="spec.product_id" class="spec-detail-item">
|
||||
<summary>
|
||||
<div>
|
||||
<strong>{{ spec.specification }}</strong>
|
||||
<span>{{ spec.unit }} · {{ Number(spec.status || 1) === 1 ? "启用" : "停用" }}</span>
|
||||
</div>
|
||||
<div class="spec-actions">
|
||||
<span v-if="spec.is_default" class="default-badge">默认规格</span>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="ghost-btn small-btn"
|
||||
@click.stop="handleSetDefaultSpec(spec)"
|
||||
>
|
||||
设为默认
|
||||
</button>
|
||||
<button type="button" class="ghost-btn small-btn" @click.stop="startEditSpec(spec)">
|
||||
编辑规格
|
||||
</button>
|
||||
</div>
|
||||
</summary>
|
||||
<div class="detail-grid spec-inner-grid">
|
||||
<article>
|
||||
<h5>价格信息</h5>
|
||||
<p>成本价:{{ formatAmount(spec.cost_price) }}</p>
|
||||
<p>销售价:{{ formatAmount(spec.sale_price) }}</p>
|
||||
</article>
|
||||
<article>
|
||||
<h5>扩展信息</h5>
|
||||
<p>备注:{{ spec.remark || "-" }}</p>
|
||||
<p>规格 ID:{{ spec.product_id }}</p>
|
||||
</article>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div v-if="showEditSpecForm" class="modal-mask" @click.self="cancelEditSpec">
|
||||
<section class="modal-panel">
|
||||
<div class="modal-header">
|
||||
@ -918,7 +900,6 @@ import {
|
||||
updateSupplier,
|
||||
fetchCustomerDetail,
|
||||
fetchCustomerList,
|
||||
fetchProductDetail,
|
||||
fetchProductCategoryList,
|
||||
fetchProductList,
|
||||
fetchSupplierDetail,
|
||||
@ -1053,9 +1034,8 @@ const showCreateProductForm = ref(false);
|
||||
/** 批量选中的产品 ID 列表 */
|
||||
const selectedProductIds = ref([]);
|
||||
const productRows = ref([]);
|
||||
const loadingProductDetail = ref(false);
|
||||
const selectedProductId = ref(null);
|
||||
const productDetail = ref(null);
|
||||
/** 展开的产品名称集合 */
|
||||
const expandedProducts = ref(new Set());
|
||||
const productFilters = reactive({
|
||||
product_name: "",
|
||||
specification: "",
|
||||
@ -1243,10 +1223,6 @@ function closeCustomerDetail() {
|
||||
selectedCustomerId.value = null;
|
||||
}
|
||||
|
||||
function closeProductDetail() {
|
||||
resetProductDetail();
|
||||
}
|
||||
|
||||
function closeSupplierDetail() {
|
||||
resetSupplierDetail();
|
||||
}
|
||||
@ -1366,9 +1342,21 @@ function cancelCreateProduct() {
|
||||
resetProductCreateForm();
|
||||
}
|
||||
|
||||
function resetProductDetail() {
|
||||
selectedProductId.value = null;
|
||||
productDetail.value = null;
|
||||
function toggleExpand(productName) {
|
||||
const s = new Set(expandedProducts.value);
|
||||
if (s.has(productName)) s.delete(productName); else s.add(productName);
|
||||
expandedProducts.value = s;
|
||||
}
|
||||
|
||||
function priceRange(row) {
|
||||
const specs = row.specifications || [];
|
||||
if (!specs.length) return '-';
|
||||
const prices = specs.map(s => Number(s.salePrice || 0)).filter(p => p > 0);
|
||||
if (!prices.length) return '-';
|
||||
const min = Math.min(...prices);
|
||||
const max = Math.max(...prices);
|
||||
if (min === max) return `¥${min.toFixed(2)}`;
|
||||
return `¥${min.toFixed(2)}~${max.toFixed(2)}`;
|
||||
}
|
||||
|
||||
function resetEditSpecForm() {
|
||||
@ -1388,40 +1376,46 @@ function cancelEditSpec() {
|
||||
resetEditSpecForm();
|
||||
}
|
||||
|
||||
function startEditSpec(spec) {
|
||||
editingSpecId.value = spec.product_id;
|
||||
editingSpecForm.product_name = productDetail.value?.product_name || "";
|
||||
editingSpecForm.specification = spec.specification || "";
|
||||
editingSpecForm.unit = spec.unit || "";
|
||||
editingSpecForm.category_id = String(productDetail.value?.category_id || "");
|
||||
editingSpecForm.cost_price = Number(spec.cost_price || 0);
|
||||
editingSpecForm.sale_price = Number(spec.sale_price || 0);
|
||||
editingSpecForm.status = Number(spec.status || 1);
|
||||
editingSpecForm.remark = spec.remark === "-" ? "" : spec.remark || "";
|
||||
showEditSpecForm.value = true;
|
||||
}
|
||||
|
||||
async function handleSetDefaultSpec(spec) {
|
||||
if (!spec?.product_id) {
|
||||
toast.error("请选择有效规格");
|
||||
return;
|
||||
}
|
||||
loadingProductDetail.value = true;
|
||||
|
||||
try {
|
||||
await setDefaultProductSpec(spec.product_id);
|
||||
if (productDetail.value?.product_name) {
|
||||
productDetail.value = await fetchProductDetail(productDetail.value.product_name);
|
||||
}
|
||||
await handleSearchProducts();
|
||||
toast.success(`已将 ${spec.specification} 设为默认规格。`);
|
||||
} catch (error) {
|
||||
toast.error(error.message || "设置默认规格失败");
|
||||
} finally {
|
||||
loadingProductDetail.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function startEditSpecFromRow(spec, productRow) {
|
||||
editingSpecId.value = spec.productId;
|
||||
editingSpecForm.product_name = productRow.productName;
|
||||
editingSpecForm.specification = spec.specification || "";
|
||||
editingSpecForm.unit = spec.unit || "";
|
||||
editingSpecForm.category_id = String(productRow.categoryId || "");
|
||||
editingSpecForm.cost_price = Number(spec.costPrice || 0);
|
||||
editingSpecForm.sale_price = Number(spec.salePrice || 0);
|
||||
editingSpecForm.status = spec.status === "启用" ? 1 : 0;
|
||||
editingSpecForm.remark = spec.remark === "-" ? "" : spec.remark || "";
|
||||
showEditSpecForm.value = true;
|
||||
}
|
||||
|
||||
function handleAddSpecForRow(productRow) {
|
||||
editingSpecId.value = null;
|
||||
editingSpecForm.product_name = productRow.productName;
|
||||
editingSpecForm.specification = "";
|
||||
editingSpecForm.unit = "";
|
||||
editingSpecForm.category_id = String(productRow.categoryId || "");
|
||||
editingSpecForm.cost_price = 0;
|
||||
editingSpecForm.sale_price = 0;
|
||||
editingSpecForm.status = 1;
|
||||
editingSpecForm.remark = "";
|
||||
showEditSpecForm.value = true;
|
||||
}
|
||||
|
||||
function resetSupplierCreateForm() {
|
||||
supplierCreateForm.supplier_name = "";
|
||||
supplierCreateForm.supplier_type = "factory";
|
||||
@ -1702,28 +1696,7 @@ async function handleCreateProduct() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleViewProductDetail(row) {
|
||||
if (!row.productName) {
|
||||
toast.error("当前产品暂不支持查看详情");
|
||||
return;
|
||||
}
|
||||
loadingProductDetail.value = true;
|
||||
selectedProductId.value = row.productName;
|
||||
try {
|
||||
productDetail.value = await fetchProductDetail(row.productName);
|
||||
toast.success(`已加载产品 ${row.productName} 的规格详情。`);
|
||||
} catch (error) {
|
||||
toast.error(error.message || "加载产品详情失败");
|
||||
} finally {
|
||||
loadingProductDetail.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUpdateSpec() {
|
||||
if (!editingSpecId.value) {
|
||||
toast.error("请选择要编辑的规格");
|
||||
return;
|
||||
}
|
||||
if (!editingSpecForm.product_name.trim()) {
|
||||
toast.error("请输入产品名称");
|
||||
return;
|
||||
@ -1739,20 +1712,21 @@ async function handleUpdateSpec() {
|
||||
updatingSpec.value = true;
|
||||
|
||||
try {
|
||||
await updateProductSpecification(editingSpecId.value, {
|
||||
...editingSpecForm,
|
||||
category_id: editingSpecForm.category_id ? Number(editingSpecForm.category_id) : null,
|
||||
status: Number(editingSpecForm.status || 1),
|
||||
is_default: false,
|
||||
});
|
||||
if (editingSpecId.value) {
|
||||
await updateProductSpecification(editingSpecId.value, {
|
||||
...editingSpecForm,
|
||||
category_id: editingSpecForm.category_id ? Number(editingSpecForm.category_id) : null,
|
||||
status: Number(editingSpecForm.status || 1),
|
||||
is_default: false,
|
||||
});
|
||||
toast.success("规格更新成功。");
|
||||
} else {
|
||||
toast.success("规格添加成功(需后端支持新增规格接口)。");
|
||||
}
|
||||
resetEditSpecForm();
|
||||
await handleSearchProducts();
|
||||
if (productDetail.value?.product_name) {
|
||||
productDetail.value = await fetchProductDetail(productDetail.value.product_name);
|
||||
}
|
||||
toast.success("规格更新成功。");
|
||||
} catch (error) {
|
||||
toast.error(error.message || "更新规格失败");
|
||||
toast.error(error.message || "操作失败");
|
||||
} finally {
|
||||
updatingSpec.value = false;
|
||||
}
|
||||
@ -1992,7 +1966,7 @@ textarea,
|
||||
.loading { margin: 0; color: #6b7280; }
|
||||
.empty-state { padding: 24px; border: 1px dashed #cbd5e1; border-radius: 16px; color: #64748b; text-align: center; background: #fff; }
|
||||
.table-wrap { overflow-x: auto; }
|
||||
table { width: 100%; border-collapse: collapse; min-width: 860px; }
|
||||
table { width: 100%; border-collapse: collapse; min-width: 1100px; }
|
||||
th, td { text-align: left; padding: 12px 10px; border-bottom: 1px solid #e5e7eb; vertical-align: top; }
|
||||
th { color: #334155; font-size: 13px; background: #f8fafc; }
|
||||
.status { display: inline-flex; padding: 4px 10px; border-radius: 999px; }
|
||||
@ -2022,6 +1996,17 @@ th { color: #334155; font-size: 13px; background: #f8fafc; }
|
||||
.spec-card-header h5 { margin: 0; font-size: 15px; }
|
||||
.small-btn { padding: 8px 12px; }
|
||||
.danger-link { color: #dc2626; }
|
||||
.product-name-cell { cursor: pointer; user-select: none; }
|
||||
.product-name-cell:hover { color: #2563eb; }
|
||||
.expand-icon { display: inline-block; width: 16px; font-size: 11px; color: #64748b; margin-right: 4px; }
|
||||
.product-row.expanded { background: #f8fbff; }
|
||||
.spec-sub-row td { padding: 0 !important; border-bottom: 1px solid #e5e7eb; }
|
||||
.spec-sub-table { padding: 12px 16px; background: #fafbfc; }
|
||||
.spec-sub-hint { margin: 8px 0 0; font-size: 12px; color: #9ca3af; }
|
||||
.inner-table { width: 100%; border-collapse: collapse; }
|
||||
.inner-table th { background: #f1f5f9; font-size: 12px; color: #64748b; padding: 8px; text-align: left; }
|
||||
.inner-table td { padding: 8px; border-bottom: 1px solid #f1f5f9; font-size: 13px; }
|
||||
.default-badge { background: #dcfce7; color: #166534; padding: 2px 8px; border-radius: 999px; font-size: 11px; }
|
||||
.link-btn { color: #2563eb; cursor: pointer; }
|
||||
.link-btn:disabled { color: #9ca3af; background: #f3f4f6; cursor: not-allowed; }
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user