Refactor frontend to Vue 3 + Vite + Pinia + Cypress E2E

- Replace single-file 8441-line HTML with Vue 3 SPA
- Pinia stores: auth, oils, recipes, diary, ui
- Composables: useApi, useDialog, useSmartPaste, useOilTranslation
- 6 shared components: RecipeCard, RecipeDetailOverlay, TagPicker, etc.
- 9 page views: RecipeSearch, RecipeManager, Inventory, OilReference, etc.
- 14 Cypress E2E test specs (113 tests), all passing
- Multi-stage Dockerfile (Node build + Python runtime)
- Demo video generation scripts (TTS + subtitles + screen recording)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-06 18:35:00 +00:00
parent 0368e85abe
commit ee8ec23dc7
62 changed files with 15035 additions and 8448 deletions

View File

@@ -0,0 +1,44 @@
const API_BASE = '' // same origin, uses vite proxy in dev
export function getToken() {
return localStorage.getItem('oil_auth_token') || ''
}
export function setToken(token) {
if (token) localStorage.setItem('oil_auth_token', token)
else localStorage.removeItem('oil_auth_token')
}
function buildHeaders(extra = {}) {
const headers = { 'Content-Type': 'application/json', ...extra }
const token = getToken()
if (token) headers['Authorization'] = 'Bearer ' + token
return headers
}
async function request(path, opts = {}) {
const headers = buildHeaders(opts.headers)
const res = await fetch(API_BASE + path, { ...opts, headers })
return res
}
async function requestJSON(path, opts = {}) {
const res = await request(path, opts)
if (!res.ok) throw res
return res.json()
}
// api is callable as api(path, opts) → raw Response
// AND has convenience methods: api.get(), api.post(), api.put(), api.delete()
function apiFn(path, opts = {}) {
return request(path, opts)
}
apiFn.raw = request
apiFn.get = (path) => requestJSON(path)
apiFn.post = (path, body) => requestJSON(path, { method: 'POST', body: JSON.stringify(body) })
apiFn.put = (path, body) => requestJSON(path, { method: 'PUT', body: JSON.stringify(body) })
apiFn.del = (path) => requestJSON(path, { method: 'DELETE' })
apiFn.delete = (path) => requestJSON(path, { method: 'DELETE' })
export const api = apiFn