From c04bb53dddcd36dc2fc5829c23646e5ce1a5603a Mon Sep 17 00:00:00 2001 From: Hera Zhao Date: Thu, 16 Apr 2026 10:56:30 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=B2=BE=E6=B2=B9=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E4=BF=9D=E5=AD=98=E5=88=B0=20DB=20+=20?= =?UTF-8?q?=E5=AF=BC=E5=87=BA=20Excel=20=E5=A2=9E=E5=8A=A0=E5=8A=9F?= =?UTF-8?q?=E6=95=88=E5=88=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 oil_cards 表,持久化知识卡片(功效/用法/方法/注意/emoji) - POST /api/oils 扩展接受 card_* 字段,在同一事务里 upsert oil_cards - GET /api/oil-cards 返回全部卡片 - 前端 getOilCard 优先查 DB,再 fallback 静态表 - saveEditOil 统一走 saveOil,不再分两套保存 - 精油价目 Excel 导出增加「功效」列 - 首次部署自动从静态 OIL_CARDS 播种到 oil_cards 表 Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/database.py | 24 ++++++++- backend/main.py | 69 ++++++++++++++++++++++++- frontend/src/composables/useOilCards.js | 12 +++++ frontend/src/stores/oils.js | 30 ++++++++++- frontend/src/views/OilReference.vue | 30 +++++------ 5 files changed, 147 insertions(+), 18 deletions(-) diff --git a/backend/database.py b/backend/database.py index e355710..c52607b 100644 --- a/backend/database.py +++ b/backend/database.py @@ -123,6 +123,15 @@ def init_db(): tag_name TEXT NOT NULL, 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 @@ -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.""" conn = get_db() c = conn.cursor() @@ -379,5 +388,18 @@ def seed_defaults(default_oils_meta: dict, default_recipes: list): (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.close() diff --git a/backend/main.py b/backend/main.py index f951b6f..438fa1a 100644 --- a/backend/main.py +++ b/backend/main.py @@ -12,6 +12,32 @@ import secrets as _secrets 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: """Convert to title case: 'pain relief' → 'Pain Relief'""" @@ -88,6 +114,12 @@ class OilIn(BaseModel): en_name: Optional[str] = None is_active: Optional[int] = 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): @@ -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)", (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, json.dumps({"bottle_price": oil.bottle_price, "drop_count": oil.drop_count})) conn.commit() @@ -752,6 +800,25 @@ def delete_oil(name: str, user=Depends(require_role("admin", "senior_editor"))): 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 ───────────────────────────────────────────── def _recipe_to_dict(conn, row): rid = row["id"] @@ -1931,7 +1998,7 @@ def startup(): if os.path.exists(defaults_path): with open(defaults_path) as 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 conn = get_db() diff --git a/frontend/src/composables/useOilCards.js b/frontend/src/composables/useOilCards.js index 9e10e67..ba98fa4 100644 --- a/frontend/src/composables/useOilCards.js +++ b/frontend/src/composables/useOilCards.js @@ -1,5 +1,6 @@ // Oil knowledge cards - usage guides for common essential oils // Ported from original vanilla JS implementation +import { useOilsStore } from '../stores/oils' 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: '轻微光敏,白天涂抹注意防晒' }, @@ -33,6 +34,17 @@ export const OIL_CARD_ALIAS = { } 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_CARD_ALIAS[name] && OIL_CARDS[OIL_CARD_ALIAS[name]]) return OIL_CARDS[OIL_CARD_ALIAS[name]] const base = name.replace(/呵护$/, '') diff --git a/frontend/src/stores/oils.js b/frontend/src/stores/oils.js index 490a78c..0c37edc 100644 --- a/frontend/src/stores/oils.js +++ b/frontend/src/stores/oils.js @@ -16,6 +16,7 @@ export const VOLUME_DROPS = { export const useOilsStore = defineStore('oils', () => { const oils = ref({}) const oilsMeta = ref({}) + const oilCards = ref({}) // Getters const oilNames = computed(() => @@ -79,9 +80,28 @@ export const useOilsStore = defineStore('oils', () => { } oils.value = newOils 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 = { name, bottle_price: bottlePrice, @@ -90,6 +110,13 @@ export const useOilsStore = defineStore('oils', () => { en_name: enName, } 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 loadOils() } @@ -138,6 +165,7 @@ export const useOilsStore = defineStore('oils', () => { return { oils, oilsMeta, + oilCards, oilNames, pricePerDrop, calcCost, diff --git a/frontend/src/views/OilReference.vue b/frontend/src/views/OilReference.vue index dadd641..816991b 100644 --- a/frontend/src/views/OilReference.vue +++ b/frontend/src/views/OilReference.vue @@ -442,7 +442,7 @@ import { useAuthStore } from '../stores/auth' import { useUiStore } from '../stores/ui' import { useRecipesStore } from '../stores/recipes' import { oilEn } from '../composables/useOilTranslation' -import { getOilCard, setOilCard } from '../composables/useOilCards' +import { getOilCard } from '../composables/useOilCards' import { showConfirm } from '../composables/useDialog' import { api } from '../composables/useApi' import { parseOilProductPaste } from '../composables/useOilProductPaste' @@ -816,26 +816,24 @@ async function saveEditOil() { } const finalDropCount = editUnit.value !== 'drop' ? editProductAmount.value : dropCount 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( newName || oldName, editBottlePrice.value, finalDropCount, editRetailPrice.value, 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 ui.showToast('已更新') editingOilName.value = null @@ -906,6 +904,7 @@ async function exportExcel() { const vol = volumeLabel(meta.dropCount, name) const unit = oilPriceUnit(name) const ppdNum = oils.pricePerDrop(name) + const card = getOilCard(name) rows.push({ '精油': name, '英文名': en, @@ -913,12 +912,13 @@ async function exportExcel() { '零售价': meta.retailPrice != null ? Number(meta.retailPrice.toFixed(2)) : '', '容量': vol, '单价': ppdNum ? `¥${ppdNum.toFixed(2)}/${unit}` : '', + '功效': card?.effects || '', '状态': meta.isActive === false ? '下架' : '在售', }) } 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() XLSX.utils.book_append_sheet(wb, ws, '精油价目表') XLSX.writeFile(wb, `精油价目表${dateStr}.xlsx`)