Compare commits
2 Commits
feat/searc
...
fix/oil-ca
| Author | SHA1 | Date | |
|---|---|---|---|
| e8af6e2565 | |||
| c04bb53ddd |
@@ -123,6 +123,15 @@ def init_db():
|
|||||||
tag_name TEXT NOT NULL,
|
tag_name TEXT NOT NULL,
|
||||||
sort_order INTEGER DEFAULT 0
|
sort_order INTEGER DEFAULT 0
|
||||||
);
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS oil_cards (
|
||||||
|
name TEXT PRIMARY KEY REFERENCES oils(name) ON DELETE CASCADE,
|
||||||
|
emoji TEXT DEFAULT '',
|
||||||
|
en TEXT DEFAULT '',
|
||||||
|
effects TEXT DEFAULT '',
|
||||||
|
usage TEXT DEFAULT '',
|
||||||
|
method TEXT DEFAULT '',
|
||||||
|
caution TEXT DEFAULT ''
|
||||||
|
);
|
||||||
""")
|
""")
|
||||||
|
|
||||||
# Migration: add password and brand fields to users if missing
|
# Migration: add password and brand fields to users if missing
|
||||||
@@ -341,7 +350,7 @@ def log_audit(conn, user_id, action, target_type=None, target_id=None, target_na
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def seed_defaults(default_oils_meta: dict, default_recipes: list):
|
def seed_defaults(default_oils_meta: dict, default_recipes: list, default_oil_cards: dict = None):
|
||||||
"""Seed DB with defaults if empty."""
|
"""Seed DB with defaults if empty."""
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
@@ -379,5 +388,18 @@ def seed_defaults(default_oils_meta: dict, default_recipes: list):
|
|||||||
(rid, tag),
|
(rid, tag),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Seed oil_cards from static data if table is empty
|
||||||
|
if default_oil_cards:
|
||||||
|
card_count = c.execute("SELECT COUNT(*) FROM oil_cards").fetchone()[0]
|
||||||
|
if card_count == 0:
|
||||||
|
for name, card in default_oil_cards.items():
|
||||||
|
c.execute(
|
||||||
|
"INSERT OR IGNORE INTO oil_cards (name, emoji, en, effects, usage, method, caution) "
|
||||||
|
"VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
(name, card.get("emoji", ""), card.get("en", ""),
|
||||||
|
card.get("effects", ""), card.get("usage", ""),
|
||||||
|
card.get("method", ""), card.get("caution", "")),
|
||||||
|
)
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|||||||
@@ -12,6 +12,32 @@ import secrets as _secrets
|
|||||||
|
|
||||||
app = FastAPI(title="Essential Oil Formula Calculator API")
|
app = FastAPI(title="Essential Oil Formula Calculator API")
|
||||||
|
|
||||||
|
# Default oil knowledge cards for DB seeding (mirrors frontend OIL_CARDS)
|
||||||
|
DEFAULT_OIL_CARDS = {
|
||||||
|
"野橘": {"emoji": "🍊", "en": "Wild Orange", "effects": "安抚镇静、驱散负能量、紧张情绪和压力\n抗氧化、消炎、抗病毒、增强免疫\n提振食欲,刺激胆汁分泌,促进消化\n促进循环", "usage": "日常香薰,提振情绪、舒压,营造愉悦氛围\n饮水加入 1至2 滴,护肝抗病毒\n泡澡时滴 3至4 滴,消除疲劳,放松身心\n扫地、拖地时加入 3至5 滴,清新空气\n涂抹腹部促进消化\n涂抹肝区帮助肝脏排毒\n口腔溃疡时加入水中漱口", "method": "🔹香薰 | 🔸内用 | 🔺涂抹", "caution": "轻微光敏,白天涂抹注意防晒"},
|
||||||
|
"冬青": {"emoji": "🌿", "en": "Wintergreen", "effects": "强效镇痛(肌肉、关节)\n抗炎、促进循环\n舒缓紧绷肌肉,抗痉挛", "usage": "牙疼时加 1 滴到水中漱口\n扭伤、落枕、酸痛(如肩颈酸痛)处稀释涂抹\n运动前后按摩", "method": "🔹香薰 |🔺涂抹(需 6 倍稀释)", "caution": "不可内用、孕期慎用、避免儿童误食"},
|
||||||
|
"生姜": {"emoji": "🫚", "en": "Ginger", "effects": "促进消化、暖胃\n活血、改善循环、祛湿\n抗炎、抗氧化、强健免疫\n缓解恶心、晕车\n促进骨骼、肌肉和关节的健康", "usage": "胀气、腹冷时,稀释涂抹腹部或喝 1 滴\n手脚冰凉时,稀释涂抹脚底或将1滴加入热饮中\n晕车时,吸闻或滴在手心嗅吸\n祛除风寒可将 2 滴加入热水中泡脚\n痛经时,稀释涂抹于小腹并按摩\n做菜时可加入 1 滴帮助增添风味", "method": "🔹香薰 | 🔸内用 | 🔺涂抹(需稀释)", "caution": ""},
|
||||||
|
"柠檬草": {"emoji": "🍃", "en": "Lemongrass", "effects": "强效抗菌、抗炎\n驱虫、净化空气\n扩张血管,促进循环,缓解肌肉疼痛", "usage": "筋膜紧绷、腿麻或肌肉酸痛时稀释涂抹\n肩周炎时,6 倍稀释后涂抹于肩颈部位并按摩\n做菜时加入 1 滴,增加泰式风味\n加入椰子油中制成家居喷雾,涂抹在裸露肌肤上驱蚊虫\n洗衣时加 3至5 滴祛味杀菌\n日常香薰平衡情绪", "method": "🔹香薰 | 🔸内用 | 🔺涂抹(需 6 倍稀释)", "caution": ""},
|
||||||
|
"柑橘清新": {"emoji": "🍬", "en": "Citrus Bliss", "effects": "提振精神,改善负面情绪\n净化空间\n降低压力", "usage": "日常香薰提升愉悦感,提振精神,净化空间\n拖地时加几滴清新空气\n加入到护手霜中,滋润手部肌肤,享受清新香气", "method": "🔹香薰 | 🔺涂抹", "caution": "含柑橘类,光敏注意白天涂抹"},
|
||||||
|
"芳香调理": {"emoji": "🤲", "en": "AromaTouch", "effects": "放松紧绷肌肉,放松关节\n促进血液循环\n促进淋巴排毒\n提升免疫\n舒缓放松,减少紧张", "usage": "稀释涂抹于太阳穴,缓解头痛,改善紧张情绪\n稀释涂抹于僵硬的身体部位如肩颈处并按摩,促进肌肉放松\n日常香薰或加入热水中泡澡,释放压力", "method": "🔹香薰 | 🔺涂抹", "caution": ""},
|
||||||
|
"西洋蓍草": {"emoji": "🔵", "en": "Yarrow | Pom", "effects": "改善肌肤老化症状\n美白肌肤,改善瑕疵\n呵护敏感肌肤,对抗炎症\n提升整体免疫", "usage": "早晚护肤时,涂抹3至4滴于面部,改善皱纹和细纹,美白肌肤\n每天早晚舌下含服1滴,促进细胞健康,提升免疫", "method": "🔸内用 | 🔺涂抹", "caution": ""},
|
||||||
|
"新瑞活力": {"emoji": "🌿", "en": "MetaPWR", "effects": "促进新陈代谢,减肥\n抑制食欲,减少对甜食的渴望\n稳定血糖波动\n提振情绪,激励身心", "usage": "饭前喝1至2滴,控制食欲,稳定血糖,提升代谢\n日常香薰可以帮助恢复能量,消除疲乏感\n稀释涂抹与身体需紧致的部位,帮助紧致塑形\n加入饮品中,帮助增添风味", "method": "🔹香薰 | 🔸内用 | 🔺涂抹(需稀释)", "caution": ""},
|
||||||
|
"安定情绪": {"emoji": "🌳", "en": "Balance", "effects": "促进全身的放松\n减轻焦虑,缓解紧张情绪\n带来宁静和安定感", "usage": "日常香薰稳定情绪,放松\n夜间香薰促进睡眠\n涂抹脚底或脊椎放松情绪,放松肌肉\n冥想、瑜伽前涂抹", "method": "🔹香薰 | 🔺涂抹", "caution": ""},
|
||||||
|
"安宁神气": {"emoji": "😴", "en": "Serenity", "effects": "促进深度睡眠\n放松身体,缓解焦虑\n平衡情绪\n平衡自律神经系统", "usage": "夜间香薰或稀释涂抹脚底促进深度睡眠,释放压力\n稀释涂抹太阳穴或脚底舒缓压力\n吸闻缓解焦虑和紧张情绪", "method": "🔹香薰 | 🔺涂抹", "caution": ""},
|
||||||
|
"元气焕能": {"emoji": "🔥", "en": "Zendocrine", "effects": "帮助身体净化,排毒\n维持肝脏和肾脏健康\n平衡情绪", "usage": "饭前内用1至2滴帮助代谢\n稀释涂抹肝区或内服3滴帮助养护肝脏\n稀释涂抹后腰脊椎出帮助养护肾脏,排除毒素\n日常香薰消除压力", "method": "🔹香薰 | 🔸内用 | 🔺涂抹", "caution": ""},
|
||||||
|
"温柔呵护": {"emoji": "🌸", "en": "Soft Talk", "effects": "平衡荷尔蒙\n抚平情绪波动\n调理经期不适\n舒缓压力\n提升女性魅力", "usage": "稀释涂抹下腹部帮助平衡荷尔蒙,或进行经期调理\n手心嗅吸帮助舒缓压力,平衡情绪\n2滴直接涂抹于脖颈后侧或手腕动脉处,提升女性魅力", "method": "🔹香薰 | 🔺涂抹", "caution": ""},
|
||||||
|
"柠檬": {"emoji": "🍋", "en": "Lemon", "effects": "清洁身体与环境\n强健免疫系统\n帮助肝脏代谢、排毒\n抗氧化\n净化空气、去异味\n蔬果清洗、保鲜\n促进循环、提振精神", "usage": "添加至护肤品中晚上使用\n添加至牙膏里美白牙齿\n滴入口中或水里喝下,一天三次,每次3至5滴,净化身体\n洗水果和蔬菜时添加 1至2 滴浸泡\n嗓子疼或感冒初期时,含服柠檬1至2滴\n日常香薰提振情绪,护肝", "method": "🔹香薰 | 🔸内用 | 🔺涂抹(夜间)", "caution": "光敏性,白天避免涂抹"},
|
||||||
|
"薰衣草": {"emoji": "💜", "en": "Lavender", "effects": "镇静安神、改善睡眠、缓解头痛\n舒缓压力、平衡情绪、抗抑郁\n烧烫伤修复、疤痕、痘印\n促进伤口修复、止血\n促进细胞再生,修复结缔组织\n抗炎、抗过敏、止痛\n皮肤舒缓止痒,如蚊虫叮咬", "usage": "烧伤、烫伤、割伤及任何伤口处涂抹,止血防疤\n夜间香薰助眠,白天香薰舒缓情绪\n鱼刺卡嗓子时滴入口中\n加入护肤品中平衡油脂、改善痘痘、去疤痕", "method": "🔹香薰 | 🔸内用 | 🔺涂抹(儿童/敏感肌需稀释)", "caution": ""},
|
||||||
|
"椒样薄荷": {"emoji": "🌿", "en": "Peppermint", "effects": "促进健康的呼吸系统\n祛痰、抗粘膜发炎、打开呼吸道\n强肝利胆,促进消化\n退热、缓解中暑\n清凉止痒\n提神醒脑、提升专注、缓解头痛", "usage": "白天香薰提神醒脑,清新空气\n按摩头部缓解头疼、提神醒脑\n蚊虫叮咬后,涂抹止痒\n混入水中进行漱口,清新口气\n发烧时涂抹额头腋下帮助降温\n打嗝、咳嗽、鼻塞时吸闻\n消化不良时稀释涂抹于腹部或内用 2 滴", "method": "🔹香薰 | 🔸内用 | 🔺涂抹(儿童/敏感肌需稀释)", "caution": "孕期/高血压慎用,晚上少用"},
|
||||||
|
"茶树": {"emoji": "🌱", "en": "Tea Tree", "effects": "抗菌、抗病毒、抗真菌\n提升免疫力\n头皮屑护理\n预防化脓\n居家杀菌净化", "usage": "各种痤疮处点涂\n加入护肤品中,清洁皮肤\n洗头时加 1 滴到洗头膏,去头皮屑\n洗衣服时加入 3至5 滴,杀菌祛味\n脚气时用茶树泡脚\n感冒时涂抹,杀菌抗病毒", "method": "🔹香薰 | 🔸内用 | 🔺涂抹(儿童/敏感肌需稀释)", "caution": ""},
|
||||||
|
"西班牙牛至": {"emoji": "🔥", "en": "Oregano", "effects": "强抗菌、抗病毒、抗顽固性真菌\n成人炎症辅助\n促进消化\n强抗氧化、抗衰老\n免疫力提升", "usage": "洗衣服或拖地时加入 3至5 滴,消炎杀菌\n吃坏肚子时灌于胶囊中内用\n灰指甲时稀释涂抹于患处\n流感季节时香薰,杀灭空气中微生物", "method": "🔹香薰 | 🔸内用(胶囊) | 🔺涂抹(需高倍稀释)", "caution": ""},
|
||||||
|
"保卫": {"emoji": "🛡", "en": "On Guard", "effects": "强化免疫力\n抗氧化\n天然杀菌、净化空气\n维护口腔健康", "usage": "日常香熏净化空气,强化免疫力\n流感季节或换季时香薰\n混入水中漱口,保持口气清新\n日常稀释涂抹于脊椎或脚底,强化免疫力\n感冒时涂抹,抗菌抗病毒", "method": "🔹香薰 | 🔸内用 | 🔺涂抹(儿童/敏感肌需稀释)", "caution": "含肉桂丁香,不宜频繁涂抹"},
|
||||||
|
"顺畅呼吸": {"emoji": "🌬", "en": "Breathe", "effects": "帮助缓解鼻炎、感冒等呼吸道不适\n促进呼吸系统健康\n净化空气", "usage": "日常香薰,强健呼吸系统,净化空气\n咳嗽、鼻塞时香薰、吸闻、涂抹于鼻翼、喉咙或肺部\n打鼾、哮喘、鼻炎可日常吸闻\n运动前吸闻,扩张呼吸道", "method": "🔹香薰 | 🔺涂抹(儿童/敏感肌需稀释)", "caution": ""},
|
||||||
|
"乐活": {"emoji": "🍃", "en": "DigestZen", "effects": "促进消化\n缓解胀气、消化不良、便秘等胃肠不适", "usage": "便秘时,稀释涂抹肚脐周围并顺时针揉腹\n喝酒前后各喝2滴,解酒护肝\n晕车时吸闻或稀释涂抹肚脐周围\n拉肚子时逆时针揉腹", "method": "🔹熏香 | 🔸内用 | 🔺涂抹(儿童/敏感肌需稀释)", "caution": ""},
|
||||||
|
"舒缓": {"emoji": "🌿", "en": "Deep Blue", "effects": "缓解肌肉酸痛\n抗痉挛,抗炎", "usage": "肌肉酸痛、扭伤、挫伤、肩颈紧绷、落枕、关节疼痛时稀释涂抹于患处", "method": "🔺涂抹(需稀释)", "caution": ""},
|
||||||
|
"乳香": {"emoji": "👑", "en": "Frankincense", "effects": "促进伤口愈合,促进细胞健康\n抗氧化、抗发炎、抗衰老、淡纹\n活血行气\n疏通血管\n滋养大脑神经", "usage": "加入护肤品中,淡斑,抗衰\n稀释后涂抹大眼眶,改善视力\n早晚舌下含服 2 滴,提高血氧含量\n夜间香薰,滋养大脑,安眠\n任何情况下,想不起来用什么就用乳香", "method": "🔹香薰 | 🔸内用 | 🔺涂抹", "caution": ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def title_case(s: str) -> str:
|
def title_case(s: str) -> str:
|
||||||
"""Convert to title case: 'pain relief' → 'Pain Relief'"""
|
"""Convert to title case: 'pain relief' → 'Pain Relief'"""
|
||||||
@@ -88,6 +114,12 @@ class OilIn(BaseModel):
|
|||||||
en_name: Optional[str] = None
|
en_name: Optional[str] = None
|
||||||
is_active: Optional[int] = None
|
is_active: Optional[int] = None
|
||||||
unit: Optional[str] = None
|
unit: Optional[str] = None
|
||||||
|
# Oil card fields (optional, saved to oil_cards table)
|
||||||
|
card_emoji: Optional[str] = None
|
||||||
|
card_effects: Optional[str] = None
|
||||||
|
card_usage: Optional[str] = None
|
||||||
|
card_method: Optional[str] = None
|
||||||
|
card_caution: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class IngredientIn(BaseModel):
|
class IngredientIn(BaseModel):
|
||||||
@@ -732,6 +764,22 @@ def upsert_oil(oil: OilIn, user=Depends(require_role("admin", "senior_editor")))
|
|||||||
"is_active=COALESCE(excluded.is_active, oils.is_active), unit=COALESCE(excluded.unit, oils.unit)",
|
"is_active=COALESCE(excluded.is_active, oils.is_active), unit=COALESCE(excluded.unit, oils.unit)",
|
||||||
(oil.name, oil.bottle_price, oil.drop_count, oil.retail_price, title_case(oil.en_name) if oil.en_name else oil.en_name, oil.is_active, oil.unit),
|
(oil.name, oil.bottle_price, oil.drop_count, oil.retail_price, title_case(oil.en_name) if oil.en_name else oil.en_name, oil.is_active, oil.unit),
|
||||||
)
|
)
|
||||||
|
# Upsert oil_cards if any card field provided
|
||||||
|
has_card = any(v is not None for v in [oil.card_emoji, oil.card_effects, oil.card_usage, oil.card_method, oil.card_caution])
|
||||||
|
if has_card:
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO oil_cards (name, emoji, en, effects, usage, method, caution) "
|
||||||
|
"VALUES (?, ?, COALESCE(?, ''), ?, ?, ?, ?) "
|
||||||
|
"ON CONFLICT(name) DO UPDATE SET "
|
||||||
|
"emoji=COALESCE(excluded.emoji, oil_cards.emoji), "
|
||||||
|
"en=COALESCE(excluded.en, oil_cards.en), "
|
||||||
|
"effects=COALESCE(excluded.effects, oil_cards.effects), "
|
||||||
|
"usage=COALESCE(excluded.usage, oil_cards.usage), "
|
||||||
|
"method=COALESCE(excluded.method, oil_cards.method), "
|
||||||
|
"caution=COALESCE(excluded.caution, oil_cards.caution)",
|
||||||
|
(oil.name, oil.card_emoji or '', title_case(oil.en_name) if oil.en_name else '',
|
||||||
|
oil.card_effects or '', oil.card_usage or '', oil.card_method or '', oil.card_caution or ''),
|
||||||
|
)
|
||||||
log_audit(conn, user["id"], "upsert_oil", "oil", oil.name, oil.name,
|
log_audit(conn, user["id"], "upsert_oil", "oil", oil.name, oil.name,
|
||||||
json.dumps({"bottle_price": oil.bottle_price, "drop_count": oil.drop_count}))
|
json.dumps({"bottle_price": oil.bottle_price, "drop_count": oil.drop_count}))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
@@ -752,6 +800,25 @@ def delete_oil(name: str, user=Depends(require_role("admin", "senior_editor"))):
|
|||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
# ── Oil Cards ──────────────────────────────────────────
|
||||||
|
@app.get("/api/oil-cards")
|
||||||
|
def list_oil_cards():
|
||||||
|
conn = get_db()
|
||||||
|
rows = conn.execute("SELECT name, emoji, en, effects, usage, method, caution FROM oil_cards ORDER BY name").fetchall()
|
||||||
|
conn.close()
|
||||||
|
return [dict(r) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/oil-cards/{name}")
|
||||||
|
def get_oil_card(name: str):
|
||||||
|
conn = get_db()
|
||||||
|
row = conn.execute("SELECT name, emoji, en, effects, usage, method, caution FROM oil_cards WHERE name = ?", (name,)).fetchone()
|
||||||
|
conn.close()
|
||||||
|
if not row:
|
||||||
|
raise HTTPException(404, "Oil card not found")
|
||||||
|
return dict(row)
|
||||||
|
|
||||||
|
|
||||||
# ── Recipes ─────────────────────────────────────────────
|
# ── Recipes ─────────────────────────────────────────────
|
||||||
def _recipe_to_dict(conn, row):
|
def _recipe_to_dict(conn, row):
|
||||||
rid = row["id"]
|
rid = row["id"]
|
||||||
@@ -1931,7 +1998,7 @@ def startup():
|
|||||||
if os.path.exists(defaults_path):
|
if os.path.exists(defaults_path):
|
||||||
with open(defaults_path) as f:
|
with open(defaults_path) as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
seed_defaults(data["oils_meta"], data["recipes"])
|
seed_defaults(data["oils_meta"], data["recipes"], DEFAULT_OIL_CARDS)
|
||||||
|
|
||||||
# One-time migration: sync display_name = username, notify about username change
|
# One-time migration: sync display_name = username, notify about username change
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Oil knowledge cards - usage guides for common essential oils
|
// Oil knowledge cards - usage guides for common essential oils
|
||||||
// Ported from original vanilla JS implementation
|
// Ported from original vanilla JS implementation
|
||||||
|
import { useOilsStore } from '../stores/oils'
|
||||||
|
|
||||||
export const OIL_CARDS = {
|
export const OIL_CARDS = {
|
||||||
'野橘': { emoji: '🍊', en: 'Wild Orange', effects: '安抚镇静、驱散负能量、紧张情绪和压力\n抗氧化、消炎、抗病毒、增强免疫\n提振食欲,刺激胆汁分泌,促进消化\n促进循环', usage: '日常香薰,提振情绪、舒压,营造愉悦氛围\n饮水加入 1至2 滴,护肝抗病毒\n泡澡时滴 3至4 滴,消除疲劳,放松身心\n扫地、拖地时加入 3至5 滴,清新空气\n涂抹腹部促进消化\n涂抹肝区帮助肝脏排毒\n口腔溃疡时加入水中漱口', method: '🔹香薰 | 🔸内用 | 🔺涂抹', caution: '轻微光敏,白天涂抹注意防晒' },
|
'野橘': { emoji: '🍊', en: 'Wild Orange', effects: '安抚镇静、驱散负能量、紧张情绪和压力\n抗氧化、消炎、抗病毒、增强免疫\n提振食欲,刺激胆汁分泌,促进消化\n促进循环', usage: '日常香薰,提振情绪、舒压,营造愉悦氛围\n饮水加入 1至2 滴,护肝抗病毒\n泡澡时滴 3至4 滴,消除疲劳,放松身心\n扫地、拖地时加入 3至5 滴,清新空气\n涂抹腹部促进消化\n涂抹肝区帮助肝脏排毒\n口腔溃疡时加入水中漱口', method: '🔹香薰 | 🔸内用 | 🔺涂抹', caution: '轻微光敏,白天涂抹注意防晒' },
|
||||||
@@ -33,6 +34,17 @@ export const OIL_CARD_ALIAS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getOilCard(name) {
|
export function getOilCard(name) {
|
||||||
|
// Check DB-persisted cards first (via store)
|
||||||
|
try {
|
||||||
|
const store = useOilsStore()
|
||||||
|
if (store.oilCards[name]) return store.oilCards[name]
|
||||||
|
// Check alias in store too
|
||||||
|
const aliased = OIL_CARD_ALIAS[name]
|
||||||
|
if (aliased && store.oilCards[aliased]) return store.oilCards[aliased]
|
||||||
|
} catch {
|
||||||
|
// Store may not be initialized yet, fall through to static data
|
||||||
|
}
|
||||||
|
// Fall back to static OIL_CARDS
|
||||||
if (OIL_CARDS[name]) return OIL_CARDS[name]
|
if (OIL_CARDS[name]) return OIL_CARDS[name]
|
||||||
if (OIL_CARD_ALIAS[name] && OIL_CARDS[OIL_CARD_ALIAS[name]]) return OIL_CARDS[OIL_CARD_ALIAS[name]]
|
if (OIL_CARD_ALIAS[name] && OIL_CARDS[OIL_CARD_ALIAS[name]]) return OIL_CARDS[OIL_CARD_ALIAS[name]]
|
||||||
const base = name.replace(/呵护$/, '')
|
const base = name.replace(/呵护$/, '')
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export const VOLUME_DROPS = {
|
|||||||
export const useOilsStore = defineStore('oils', () => {
|
export const useOilsStore = defineStore('oils', () => {
|
||||||
const oils = ref({})
|
const oils = ref({})
|
||||||
const oilsMeta = ref({})
|
const oilsMeta = ref({})
|
||||||
|
const oilCards = ref({})
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
const oilNames = computed(() =>
|
const oilNames = computed(() =>
|
||||||
@@ -79,9 +80,28 @@ export const useOilsStore = defineStore('oils', () => {
|
|||||||
}
|
}
|
||||||
oils.value = newOils
|
oils.value = newOils
|
||||||
oilsMeta.value = newMeta
|
oilsMeta.value = newMeta
|
||||||
|
|
||||||
|
// Also fetch oil cards from DB
|
||||||
|
try {
|
||||||
|
const cards = await api.get('/api/oil-cards')
|
||||||
|
const newCards = {}
|
||||||
|
for (const card of cards) {
|
||||||
|
newCards[card.name] = {
|
||||||
|
emoji: card.emoji || '',
|
||||||
|
en: card.en || '',
|
||||||
|
effects: card.effects || '',
|
||||||
|
usage: card.usage || '',
|
||||||
|
method: card.method || '',
|
||||||
|
caution: card.caution || '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
oilCards.value = newCards
|
||||||
|
} catch {
|
||||||
|
// oil_cards table may not exist yet on older backends
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveOil(name, bottlePrice, dropCount, retailPrice, enName = null, unit = null) {
|
async function saveOil(name, bottlePrice, dropCount, retailPrice, enName = null, unit = null, card = null) {
|
||||||
const payload = {
|
const payload = {
|
||||||
name,
|
name,
|
||||||
bottle_price: bottlePrice,
|
bottle_price: bottlePrice,
|
||||||
@@ -90,6 +110,13 @@ export const useOilsStore = defineStore('oils', () => {
|
|||||||
en_name: enName,
|
en_name: enName,
|
||||||
}
|
}
|
||||||
if (unit) payload.unit = unit
|
if (unit) payload.unit = unit
|
||||||
|
if (card) {
|
||||||
|
payload.card_emoji = card.emoji ?? null
|
||||||
|
payload.card_effects = card.effects ?? null
|
||||||
|
payload.card_usage = card.usage ?? null
|
||||||
|
payload.card_method = card.method ?? null
|
||||||
|
payload.card_caution = card.caution ?? null
|
||||||
|
}
|
||||||
await api.post('/api/oils', payload)
|
await api.post('/api/oils', payload)
|
||||||
await loadOils()
|
await loadOils()
|
||||||
}
|
}
|
||||||
@@ -138,6 +165,7 @@ export const useOilsStore = defineStore('oils', () => {
|
|||||||
return {
|
return {
|
||||||
oils,
|
oils,
|
||||||
oilsMeta,
|
oilsMeta,
|
||||||
|
oilCards,
|
||||||
oilNames,
|
oilNames,
|
||||||
pricePerDrop,
|
pricePerDrop,
|
||||||
calcCost,
|
calcCost,
|
||||||
|
|||||||
@@ -442,7 +442,7 @@ import { useAuthStore } from '../stores/auth'
|
|||||||
import { useUiStore } from '../stores/ui'
|
import { useUiStore } from '../stores/ui'
|
||||||
import { useRecipesStore } from '../stores/recipes'
|
import { useRecipesStore } from '../stores/recipes'
|
||||||
import { oilEn } from '../composables/useOilTranslation'
|
import { oilEn } from '../composables/useOilTranslation'
|
||||||
import { getOilCard, setOilCard } from '../composables/useOilCards'
|
import { getOilCard } from '../composables/useOilCards'
|
||||||
import { showConfirm } from '../composables/useDialog'
|
import { showConfirm } from '../composables/useDialog'
|
||||||
import { api } from '../composables/useApi'
|
import { api } from '../composables/useApi'
|
||||||
import { parseOilProductPaste } from '../composables/useOilProductPaste'
|
import { parseOilProductPaste } from '../composables/useOilProductPaste'
|
||||||
@@ -816,26 +816,24 @@ async function saveEditOil() {
|
|||||||
}
|
}
|
||||||
const finalDropCount = editUnit.value !== 'drop' ? editProductAmount.value : dropCount
|
const finalDropCount = editUnit.value !== 'drop' ? editProductAmount.value : dropCount
|
||||||
const finalUnit = editUnit.value !== 'drop' ? editProductUnit.value : null
|
const finalUnit = editUnit.value !== 'drop' ? editProductUnit.value : null
|
||||||
|
// Build card payload if any card content provided
|
||||||
|
const hasCard = editCardEffects.value.trim() || editCardUsage.value.trim()
|
||||||
|
const cardPayload = hasCard ? {
|
||||||
|
emoji: editCardEmoji.value || '🌿',
|
||||||
|
effects: editCardEffects.value.trim(),
|
||||||
|
usage: editCardUsage.value.trim(),
|
||||||
|
method: editCardMethod.value.trim(),
|
||||||
|
caution: editCardCaution.value.trim(),
|
||||||
|
} : null
|
||||||
await oils.saveOil(
|
await oils.saveOil(
|
||||||
newName || oldName,
|
newName || oldName,
|
||||||
editBottlePrice.value,
|
editBottlePrice.value,
|
||||||
finalDropCount,
|
finalDropCount,
|
||||||
editRetailPrice.value,
|
editRetailPrice.value,
|
||||||
editOilEnName.value.trim() || null,
|
editOilEnName.value.trim() || null,
|
||||||
finalUnit
|
finalUnit,
|
||||||
|
cardPayload
|
||||||
)
|
)
|
||||||
// Save knowledge card if any content provided
|
|
||||||
const finalName = newName || oldName
|
|
||||||
if (editCardEffects.value.trim() || editCardUsage.value.trim()) {
|
|
||||||
setOilCard(finalName, {
|
|
||||||
emoji: editCardEmoji.value || '🌿',
|
|
||||||
en: editOilEnName.value.trim() || '',
|
|
||||||
effects: editCardEffects.value.trim(),
|
|
||||||
usage: editCardUsage.value.trim(),
|
|
||||||
method: editCardMethod.value.trim(),
|
|
||||||
caution: editCardCaution.value.trim(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
cardVersion.value++ // trigger re-render for card badges
|
cardVersion.value++ // trigger re-render for card badges
|
||||||
ui.showToast('已更新')
|
ui.showToast('已更新')
|
||||||
editingOilName.value = null
|
editingOilName.value = null
|
||||||
@@ -902,10 +900,11 @@ async function exportExcel() {
|
|||||||
for (const name of sortedNames) {
|
for (const name of sortedNames) {
|
||||||
const meta = getMeta(name)
|
const meta = getMeta(name)
|
||||||
if (!meta) continue
|
if (!meta) continue
|
||||||
const en = getEnglishName(name)
|
const en = meta.enName || getEnglishName(name)
|
||||||
const vol = volumeLabel(meta.dropCount, name)
|
const vol = volumeLabel(meta.dropCount, name)
|
||||||
const unit = oilPriceUnit(name)
|
const unit = oilPriceUnit(name)
|
||||||
const ppdNum = oils.pricePerDrop(name)
|
const ppdNum = oils.pricePerDrop(name)
|
||||||
|
const card = getOilCard(name)
|
||||||
rows.push({
|
rows.push({
|
||||||
'精油': name,
|
'精油': name,
|
||||||
'英文名': en,
|
'英文名': en,
|
||||||
@@ -913,12 +912,13 @@ async function exportExcel() {
|
|||||||
'零售价': meta.retailPrice != null ? Number(meta.retailPrice.toFixed(2)) : '',
|
'零售价': meta.retailPrice != null ? Number(meta.retailPrice.toFixed(2)) : '',
|
||||||
'容量': vol,
|
'容量': vol,
|
||||||
'单价': ppdNum ? `¥${ppdNum.toFixed(2)}/${unit}` : '',
|
'单价': ppdNum ? `¥${ppdNum.toFixed(2)}/${unit}` : '',
|
||||||
|
'功效': card?.effects || '',
|
||||||
'状态': meta.isActive === false ? '下架' : '在售',
|
'状态': meta.isActive === false ? '下架' : '在售',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const ws = XLSX.utils.json_to_sheet(rows)
|
const ws = XLSX.utils.json_to_sheet(rows)
|
||||||
ws['!cols'] = [{ wch: 16 }, { wch: 28 }, { wch: 10 }, { wch: 10 }, { wch: 12 }, { wch: 16 }, { wch: 8 }]
|
ws['!cols'] = [{ wch: 16 }, { wch: 28 }, { wch: 10 }, { wch: 10 }, { wch: 12 }, { wch: 16 }, { wch: 40 }, { wch: 8 }]
|
||||||
const wb = XLSX.utils.book_new()
|
const wb = XLSX.utils.book_new()
|
||||||
XLSX.utils.book_append_sheet(wb, ws, '精油价目表')
|
XLSX.utils.book_append_sheet(wb, ws, '精油价目表')
|
||||||
XLSX.writeFile(wb, `精油价目表${dateStr}.xlsx`)
|
XLSX.writeFile(wb, `精油价目表${dateStr}.xlsx`)
|
||||||
|
|||||||
Reference in New Issue
Block a user