diff --git a/backend/main.py b/backend/main.py
index 9786964..746bcc9 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -80,6 +80,7 @@ class OilIn(BaseModel):
drop_count: int
retail_price: Optional[float] = None
en_name: Optional[str] = None
+ is_active: Optional[int] = None
class IngredientIn(BaseModel):
@@ -659,10 +660,11 @@ def list_oils():
def upsert_oil(oil: OilIn, user=Depends(require_role("admin", "senior_editor"))):
conn = get_db()
conn.execute(
- "INSERT INTO oils (name, bottle_price, drop_count, retail_price, en_name) VALUES (?, ?, ?, ?, ?) "
+ "INSERT INTO oils (name, bottle_price, drop_count, retail_price, en_name, is_active) VALUES (?, ?, ?, ?, ?, ?) "
"ON CONFLICT(name) DO UPDATE SET bottle_price=excluded.bottle_price, drop_count=excluded.drop_count, "
- "retail_price=excluded.retail_price, en_name=COALESCE(excluded.en_name, oils.en_name)",
- (oil.name, oil.bottle_price, oil.drop_count, oil.retail_price, oil.en_name),
+ "retail_price=excluded.retail_price, en_name=COALESCE(excluded.en_name, oils.en_name), "
+ "is_active=COALESCE(excluded.is_active, oils.is_active)",
+ (oil.name, oil.bottle_price, oil.drop_count, oil.retail_price, oil.en_name, oil.is_active),
)
log_audit(conn, user["id"], "upsert_oil", "oil", oil.name, oil.name,
json.dumps({"bottle_price": oil.bottle_price, "drop_count": oil.drop_count}))
diff --git a/frontend/src/views/OilReference.vue b/frontend/src/views/OilReference.vue
index aec177d..805d171 100644
--- a/frontend/src/views/OilReference.vue
+++ b/frontend/src/views/OilReference.vue
@@ -124,23 +124,20 @@
v-for="name in filteredOilNames"
:key="name + '-' + cardVersion"
class="oil-chip"
+ :class="{ 'oil-chip--inactive': getMeta(name)?.isActive === false, 'oil-chip--incomplete': auth.isAdmin && isIncomplete(name) }"
:style="chipStyle(name)"
@click="openOilDetail(name)"
>
-
{{ name }}
- 📖
-
-
-
{{ getEnglishName(name) }}
+
{{ name }}
+
{{ getEnglishName(name) }}
¥{{ (getMeta(name)?.bottlePrice || 0).toFixed(0) }}/瓶
- {{ volumeLabel(getMeta(name).dropCount) }}
- ¥{{ getMeta(name).retailPrice }}
+ ¥{{ getMeta(name).retailPrice }}/瓶
@@ -331,9 +328,15 @@
-
-
-
+
+
+
+
+
+
@@ -466,14 +469,18 @@ function volumeLabel(dropCount, name) {
}
function chipStyle(name) {
- const meta = getMeta(name)
- const isActive = meta?.isActive !== false
const hasCard = !!getOilCard(name)
- if (!isActive) return 'opacity:0.7;background:#f5f5f5'
if (hasCard) return 'cursor:pointer;border-left:3px solid var(--sage);background:linear-gradient(90deg,var(--sage-mist),white)'
return ''
}
+function isIncomplete(name) {
+ const meta = getMeta(name)
+ if (!meta) return true
+ // Incomplete if missing bottle price, drop count, or retail price
+ return !meta.bottlePrice || !meta.dropCount || !meta.retailPrice
+}
+
function getEffectiveDropCount() {
if (newVolume.value === 'custom') return newCustomDrops.value || 0
return VOLUME_OPTIONS[newVolume.value] || 0
@@ -662,6 +669,31 @@ async function saveEditOil() {
}
}
+async function toggleOilActive() {
+ const name = editingOilName.value
+ if (!name) return
+ const meta = getMeta(name)
+ const newActive = meta?.isActive === false ? 1 : 0
+ try {
+ await api('/api/oils', {
+ method: 'POST',
+ body: JSON.stringify({
+ name,
+ bottle_price: meta?.bottlePrice || 0,
+ drop_count: meta?.dropCount || 1,
+ retail_price: meta?.retailPrice,
+ is_active: newActive,
+ }),
+ })
+ await oils.loadOils()
+ cardVersion.value++
+ ui.showToast(newActive ? '已重新上架' : '已下架')
+ editingOilName.value = null
+ } catch {
+ ui.showToast('操作失败')
+ }
+}
+
async function removeOil(name) {
const ok = await showConfirm(`确定删除精油 "${name}"?`)
if (!ok) return
@@ -1161,6 +1193,33 @@ async function saveCardImage(name) {
.oil-chip:hover {
box-shadow: 0 4px 16px rgba(90,60,30,0.12);
}
+.oil-chip--inactive {
+ opacity: 0.5;
+ background: #f0f0f0 !important;
+}
+.oil-chip--incomplete {
+ background: #fff5f5 !important;
+}
+.oil-name-line {
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--text-dark);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.oil-en-line {
+ font-size: 10px;
+ color: var(--text-light);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+/* Shrink name for long oils */
+@container (max-width: 180px) {
+ .oil-name-line { font-size: 12px; }
+ .oil-en-line { font-size: 9px; }
+}
.oil-chip-actions {
position: absolute;