Compare commits

...

4 Commits

Author SHA1 Message Date
b429cd1264 ci: retrigger after CI backend crash
Some checks failed
PR Preview / test (pull_request) Has been skipped
Deploy Production / test (push) Successful in 6s
PR Preview / teardown-preview (pull_request) Successful in 13s
PR Preview / deploy-preview (pull_request) Has been skipped
Test / unit-test (push) Successful in 5s
Test / build-check (push) Successful in 3s
Deploy Production / deploy (push) Successful in 5s
Test / e2e-test (push) Failing after 6m1s
2026-04-16 11:32:43 +00:00
5bc3600384 fix: 精油价目 Excel 导出补全所有卡片字段
Some checks failed
Test / unit-test (push) Successful in 7s
PR Preview / teardown-preview (pull_request) Has been skipped
Test / build-check (push) Successful in 4s
PR Preview / test (pull_request) Successful in 6s
PR Preview / deploy-preview (pull_request) Successful in 14s
Test / e2e-test (push) Failing after 45s
列:功效、使用方法、使用方式(香薰/涂抹/内用)、注意事项

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 11:18:12 +00:00
e8af6e2565 fix: Excel 导出英文名也直接取 DB meta.enName
All checks were successful
PR Preview / test (pull_request) Has been skipped
Deploy Production / test (push) Successful in 7s
PR Preview / teardown-preview (pull_request) Successful in 14s
PR Preview / deploy-preview (pull_request) Has been skipped
Test / unit-test (push) Successful in 6s
Test / build-check (push) Successful in 4s
Deploy Production / deploy (push) Successful in 6s
Test / e2e-test (push) Successful in 3m2s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 11:01:19 +00:00
c04bb53ddd fix: 精油编辑统一保存到 DB + 导出 Excel 增加功效列
Some checks failed
Test / unit-test (push) Successful in 7s
Test / build-check (push) Successful in 4s
PR Preview / teardown-preview (pull_request) Has been skipped
Test / e2e-test (push) Failing after 1m12s
PR Preview / test (pull_request) Successful in 7s
PR Preview / deploy-preview (pull_request) Successful in 19s
- 新增 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) <noreply@anthropic.com>
2026-04-16 10:56:30 +00:00
5 changed files with 151 additions and 19 deletions

View File

@@ -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()

View File

@@ -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()

View File

@@ -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(/呵护$/, '')

View File

@@ -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,

View File

@@ -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
@@ -902,10 +900,11 @@ async function exportExcel() {
for (const name of sortedNames) {
const meta = getMeta(name)
if (!meta) continue
const en = getEnglishName(name)
const en = meta.enName || getEnglishName(name)
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,16 @@ async function exportExcel() {
'零售价': meta.retailPrice != null ? Number(meta.retailPrice.toFixed(2)) : '',
'容量': vol,
'单价': ppdNum ? `¥${ppdNum.toFixed(2)}/${unit}` : '',
'功效': card?.effects || '',
'使用方法': card?.usage || '',
'使用方式': card?.method || '',
'注意事项': card?.caution || '',
'状态': 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: 40 }, { wch: 20 }, { wch: 24 }, { wch: 8 }]
const wb = XLSX.utils.book_new()
XLSX.utils.book_append_sheet(wb, ws, '精油价目表')
XLSX.writeFile(wb, `精油价目表${dateStr}.xlsx`)