Compare commits
5 Commits
feat/ci-fi
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| e605da786a | |||
| 87e24773aa | |||
| 026ff18e92 | |||
| 6448c24caf | |||
| 5cd954ccad |
@@ -58,9 +58,9 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Run core cypress specs with timeouts
|
# Run core cypress specs with hard 3-minute timeout
|
||||||
cd frontend
|
cd frontend
|
||||||
npx cypress run --spec "\
|
timeout 180 npx cypress run --spec "\
|
||||||
cypress/e2e/recipe-detail.cy.js,\
|
cypress/e2e/recipe-detail.cy.js,\
|
||||||
cypress/e2e/oil-reference.cy.js,\
|
cypress/e2e/oil-reference.cy.js,\
|
||||||
cypress/e2e/oil-data-integrity.cy.js,\
|
cypress/e2e/oil-data-integrity.cy.js,\
|
||||||
@@ -68,12 +68,17 @@ jobs:
|
|||||||
cypress/e2e/category-modules.cy.js,\
|
cypress/e2e/category-modules.cy.js,\
|
||||||
cypress/e2e/notification-flow.cy.js,\
|
cypress/e2e/notification-flow.cy.js,\
|
||||||
cypress/e2e/registration-flow.cy.js\
|
cypress/e2e/registration-flow.cy.js\
|
||||||
" --config "video=false,defaultCommandTimeout=5000,pageLoadTimeout=10000,baseUrl=http://localhost:$FE_PORT"
|
" --config "video=false,defaultCommandTimeout=5000,pageLoadTimeout=10000,requestTimeout=5000,responseTimeout=10000,baseUrl=http://localhost:$FE_PORT"
|
||||||
EXIT_CODE=$?
|
EXIT_CODE=$?
|
||||||
|
|
||||||
# Cleanup
|
# Cleanup
|
||||||
kill $BE_PID $FE_PID 2>/dev/null
|
kill $BE_PID $FE_PID 2>/dev/null
|
||||||
|
pkill -f "Cypress" 2>/dev/null || true
|
||||||
rm -f "$DB_FILE"
|
rm -f "$DB_FILE"
|
||||||
|
if [ $EXIT_CODE -eq 124 ]; then
|
||||||
|
echo "ERROR: Cypress timed out after 3 minutes"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
exit $EXIT_CODE
|
exit $EXIT_CODE
|
||||||
|
|
||||||
build-check:
|
build-check:
|
||||||
|
|||||||
@@ -85,3 +85,51 @@ describe('EDITOR_ONLY_TAGS', () => {
|
|||||||
expect(EDITOR_ONLY_TAGS).toContain('已审核')
|
expect(EDITOR_ONLY_TAGS).toContain('已审核')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// English search
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
describe('English search matching', () => {
|
||||||
|
const { oilEn } = require('../composables/useOilTranslation')
|
||||||
|
|
||||||
|
it('oilEn returns English name for known oils', () => {
|
||||||
|
expect(oilEn('薰衣草')).toBe('Lavender')
|
||||||
|
expect(oilEn('茶树')).toBe('Tea Tree')
|
||||||
|
expect(oilEn('乳香')).toBe('Frankincense')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('oilEn returns empty for unknown oils', () => {
|
||||||
|
expect(oilEn('不存在的油')).toBeFalsy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('English query detection', () => {
|
||||||
|
const isEn = (q) => /^[a-zA-Z\s]+$/.test(q)
|
||||||
|
expect(isEn('lavender')).toBe(true)
|
||||||
|
expect(isEn('Tea Tree')).toBe(true)
|
||||||
|
expect(isEn('薰衣草')).toBe(false)
|
||||||
|
expect(isEn('lav3')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('English matches oil name in recipe', () => {
|
||||||
|
const recipe = {
|
||||||
|
name: '助眠配方',
|
||||||
|
en_name: 'Sleep Aid Blend',
|
||||||
|
ingredients: [{ oil: '薰衣草', drops: 3 }],
|
||||||
|
tags: []
|
||||||
|
}
|
||||||
|
const q = 'lavender'
|
||||||
|
const isEn = /^[a-zA-Z\s]+$/.test(q)
|
||||||
|
const enNameMatch = isEn && (recipe.en_name || '').toLowerCase().includes(q)
|
||||||
|
const oilEnMatch = isEn && recipe.ingredients.some(ing => (oilEn(ing.oil) || '').toLowerCase().includes(q))
|
||||||
|
expect(oilEnMatch).toBe(true)
|
||||||
|
expect(enNameMatch).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('English matches recipe en_name', () => {
|
||||||
|
const recipe = { name: '助眠', en_name: 'Sleep Aid Blend', ingredients: [], tags: [] }
|
||||||
|
const q = 'sleep'
|
||||||
|
const isEn = /^[a-zA-Z\s]+$/.test(q)
|
||||||
|
const enNameMatch = isEn && (recipe.en_name || '').toLowerCase().includes(q)
|
||||||
|
expect(enNameMatch).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -175,6 +175,7 @@ import { useUiStore } from '../stores/ui'
|
|||||||
import { api } from '../composables/useApi'
|
import { api } from '../composables/useApi'
|
||||||
import RecipeCard from '../components/RecipeCard.vue'
|
import RecipeCard from '../components/RecipeCard.vue'
|
||||||
import RecipeDetailOverlay from '../components/RecipeDetailOverlay.vue'
|
import RecipeDetailOverlay from '../components/RecipeDetailOverlay.vue'
|
||||||
|
import { oilEn } from '../composables/useOilTranslation'
|
||||||
|
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
const oils = useOilsStore()
|
const oils = useOilsStore()
|
||||||
@@ -313,11 +314,14 @@ function expandQuery(q) {
|
|||||||
const exactResults = computed(() => {
|
const exactResults = computed(() => {
|
||||||
if (!searchQuery.value.trim()) return []
|
if (!searchQuery.value.trim()) return []
|
||||||
const q = searchQuery.value.trim().toLowerCase()
|
const q = searchQuery.value.trim().toLowerCase()
|
||||||
|
const isEn = /^[a-zA-Z\s]+$/.test(q)
|
||||||
return recipeStore.recipes.filter(r => {
|
return recipeStore.recipes.filter(r => {
|
||||||
const nameMatch = r.name.toLowerCase().includes(q)
|
const nameMatch = r.name.toLowerCase().includes(q)
|
||||||
|
const enNameMatch = isEn && (r.en_name || '').toLowerCase().includes(q)
|
||||||
|
const oilEnMatch = isEn && r.ingredients.some(ing => (oilEn(ing.oil) || '').toLowerCase().includes(q))
|
||||||
const visibleTags = auth.canEdit ? (r.tags || []) : (r.tags || []).filter(t => !EDITOR_ONLY_TAGS.includes(t))
|
const visibleTags = auth.canEdit ? (r.tags || []) : (r.tags || []).filter(t => !EDITOR_ONLY_TAGS.includes(t))
|
||||||
const tagMatch = visibleTags.some(t => t.toLowerCase().includes(q))
|
const tagMatch = visibleTags.some(t => t.toLowerCase().includes(q))
|
||||||
return nameMatch || tagMatch
|
return nameMatch || enNameMatch || oilEnMatch || tagMatch
|
||||||
}).sort((a, b) => a.name.localeCompare(b.name, 'zh'))
|
}).sort((a, b) => a.name.localeCompare(b.name, 'zh'))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
<span class="review-name">{{ group.latest.display_name || group.latest.username }}</span>
|
<span class="review-name">{{ group.latest.display_name || group.latest.username }}</span>
|
||||||
<span class="review-reason">商户名:{{ group.latest.business_name }}</span>
|
<span class="review-reason">商户名:{{ group.latest.business_name }}</span>
|
||||||
<span class="biz-status-tag" :class="'biz-' + group.effectiveStatus">{{ { pending: '待审核', approved: '已通过', rejected: '已拒绝' }[group.effectiveStatus] }}</span>
|
<span class="biz-status-tag" :class="'biz-' + group.effectiveStatus">{{ { pending: '待审核', approved: '已通过', rejected: '已拒绝' }[group.effectiveStatus] }}</span>
|
||||||
|
<img v-if="group.latest.document && group.latest.document.startsWith('data:image')" :src="group.latest.document" class="biz-doc-preview" @click="showDocFull = group.latest.document" />
|
||||||
</div>
|
</div>
|
||||||
<div class="review-actions">
|
<div class="review-actions">
|
||||||
<template v-if="group.effectiveStatus === 'pending'">
|
<template v-if="group.effectiveStatus === 'pending'">
|
||||||
@@ -111,6 +112,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="user-count">共 {{ users.length }} 个用户</div>
|
<div class="user-count">共 {{ users.length }} 个用户</div>
|
||||||
|
|
||||||
|
<!-- Full-size document preview -->
|
||||||
|
<div v-if="showDocFull" class="doc-overlay" @click="showDocFull = null">
|
||||||
|
<img :src="showDocFull" class="doc-full-img" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -128,6 +134,7 @@ const users = ref([])
|
|||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
const filterRole = ref('')
|
const filterRole = ref('')
|
||||||
const translations = ref([])
|
const translations = ref([])
|
||||||
|
const showDocFull = ref(null)
|
||||||
const businessApps = ref([])
|
const businessApps = ref([])
|
||||||
import { reactive } from 'vue'
|
import { reactive } from 'vue'
|
||||||
|
|
||||||
@@ -443,6 +450,10 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
.biz-reject-reason { color: #c62828; font-size: 11px; }
|
.biz-reject-reason { color: #c62828; font-size: 11px; }
|
||||||
.biz-time { color: #bbb; font-size: 11px; margin-left: auto; }
|
.biz-time { color: #bbb; font-size: 11px; margin-left: auto; }
|
||||||
|
.biz-doc-preview { width: 60px; height: 60px; object-fit: cover; border-radius: 6px; cursor: pointer; border: 1px solid #e5e4e7; margin-top: 6px; }
|
||||||
|
.biz-doc-preview:hover { border-color: #7ec6a4; }
|
||||||
|
.doc-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.7); z-index: 1000; display: flex; align-items: center; justify-content: center; cursor: pointer; }
|
||||||
|
.doc-full-img { max-width: 90vw; max-height: 90vh; border-radius: 10px; }
|
||||||
|
|
||||||
.btn-approve {
|
.btn-approve {
|
||||||
background: #4a9d7e;
|
background: #4a9d7e;
|
||||||
|
|||||||
Reference in New Issue
Block a user