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

103
frontend/src/stores/auth.js Normal file
View File

@@ -0,0 +1,103 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { api } from '../composables/useApi'
const DEFAULT_USER = {
id: null,
role: 'viewer',
username: 'anonymous',
display_name: '匿名',
has_password: false,
business_verified: false,
}
export const useAuthStore = defineStore('auth', () => {
const token = ref(localStorage.getItem('oil_auth_token') || '')
const user = ref({ ...DEFAULT_USER })
// Getters
const isLoggedIn = computed(() => user.value.id !== null)
const isAdmin = computed(() => user.value.role === 'admin')
const canEdit = computed(() =>
['editor', 'senior_editor', 'admin'].includes(user.value.role)
)
const isBusiness = computed(() => user.value.business_verified)
// Actions
async function initToken() {
const params = new URLSearchParams(window.location.search)
const urlToken = params.get('token')
if (urlToken) {
token.value = urlToken
localStorage.setItem('oil_auth_token', urlToken)
// Clean URL
const url = new URL(window.location)
url.searchParams.delete('token')
window.history.replaceState({}, '', url)
}
if (token.value) {
await loadMe()
}
}
async function loadMe() {
try {
const data = await api.get('/api/me')
user.value = {
id: data.id,
role: data.role,
username: data.username,
display_name: data.display_name,
has_password: data.has_password ?? false,
business_verified: data.business_verified ?? false,
}
} catch {
logout()
}
}
async function login(username, password) {
const data = await api.post('/api/login', { username, password })
token.value = data.token
localStorage.setItem('oil_auth_token', data.token)
await loadMe()
}
async function register(username, password, displayName) {
const data = await api.post('/api/register', {
username,
password,
display_name: displayName,
})
token.value = data.token
localStorage.setItem('oil_auth_token', data.token)
await loadMe()
}
function logout() {
token.value = ''
localStorage.removeItem('oil_auth_token')
user.value = { ...DEFAULT_USER }
}
function canEditRecipe(recipe) {
if (isAdmin.value || user.value.role === 'senior_editor') return true
if (user.value.role === 'editor' && recipe._owner_id === user.value.id) return true
return false
}
return {
token,
user,
isLoggedIn,
isAdmin,
canEdit,
isBusiness,
initToken,
loadMe,
login,
register,
logout,
canEditRecipe,
}
})