优化
This commit is contained in:
parent
4ec17d81f9
commit
af484866ee
|
|
@ -1,11 +1,34 @@
|
|||
<script setup lang="ts">
|
||||
import { onLaunch, onShow, onHide } from '@dcloudio/uni-app'
|
||||
import { initCompatibilityChecks } from '@/utils/compatibility'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { get } from '@/utils/api'
|
||||
|
||||
onLaunch(() => {
|
||||
onLaunch(async () => {
|
||||
console.log('App Launch')
|
||||
// 初始化兼容性检查(需求 11.1, 11.2)
|
||||
initCompatibilityChecks()
|
||||
|
||||
// 恢复登录状态:token 存在时拉取用户信息
|
||||
const token = uni.getStorageSync('token')
|
||||
if (token) {
|
||||
try {
|
||||
const res = await get<{
|
||||
openId: string
|
||||
nickname: string
|
||||
avatarUrl: string
|
||||
totalCats: number
|
||||
totalLikes: number
|
||||
totalFavorites: number
|
||||
}>('/user/profile')
|
||||
if (res.success && res.data) {
|
||||
const userStore = useUserStore()
|
||||
userStore.setUser(res.data)
|
||||
}
|
||||
} catch {
|
||||
// token 过期或无效,清除
|
||||
uni.removeStorageSync('token')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
|
|
|
|||
|
|
@ -88,9 +88,10 @@ const shadowStyle = computed(() => ({
|
|||
}))
|
||||
|
||||
// 动画循环(需求 5.1)
|
||||
const animate = (timestamp: number) => {
|
||||
const animate = () => {
|
||||
const timestamp = Date.now()
|
||||
|
||||
if (!props.active) {
|
||||
animationFrame.value = requestAnimationFrame(animate)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -122,22 +123,20 @@ const animate = (timestamp: number) => {
|
|||
|
||||
// 耳朵运动动画(需求 5.1)
|
||||
earAngle.value = Math.sin(timestamp / 200) * 2
|
||||
|
||||
animationFrame.value = requestAnimationFrame(animate)
|
||||
}
|
||||
|
||||
// 开始动画
|
||||
const startAnimation = () => {
|
||||
if (animationFrame.value === null) {
|
||||
lastTime.value = 0
|
||||
animationFrame.value = requestAnimationFrame(animate)
|
||||
animationFrame.value = setInterval(animate, 16) as unknown as number
|
||||
}
|
||||
}
|
||||
|
||||
// 停止动画
|
||||
const stopAnimation = () => {
|
||||
if (animationFrame.value !== null) {
|
||||
cancelAnimationFrame(animationFrame.value)
|
||||
clearInterval(animationFrame.value)
|
||||
animationFrame.value = null
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,8 +89,8 @@ const initCanvas = () => {
|
|||
ctx.lineCap = 'round'
|
||||
ctx.lineJoin = 'round'
|
||||
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.fillRect(0, 0, props.width, props.height)
|
||||
// 透明背景 - 不填充白色
|
||||
ctx.clearRect(0, 0, props.width, props.height)
|
||||
|
||||
console.log('Canvas initialized successfully')
|
||||
} else {
|
||||
|
|
@ -107,6 +107,22 @@ const drawLine = (from: { x: number; y: number }, to: { x: number; y: number })
|
|||
|
||||
const style = getCurrentStrokeStyle()
|
||||
|
||||
if (canvasStore.isEraser) {
|
||||
// 橡皮擦用 clearRect 擦除为透明
|
||||
const size = style.size
|
||||
const dx = to.x - from.x
|
||||
const dy = to.y - from.y
|
||||
const dist = Math.sqrt(dx * dx + dy * dy)
|
||||
const steps = Math.max(1, Math.floor(dist / 2))
|
||||
for (let i = 0; i <= steps; i++) {
|
||||
const t = i / steps
|
||||
const x = from.x + dx * t - size / 2
|
||||
const y = from.y + dy * t - size / 2
|
||||
ctx.clearRect(x, y, size, size)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.strokeStyle = style.color
|
||||
ctx.lineWidth = style.size
|
||||
|
|
@ -144,11 +160,16 @@ const handleTouchStart = (e: any) => {
|
|||
lastPoint.value = pos
|
||||
|
||||
if (ctx) {
|
||||
const style = getCurrentStrokeStyle()
|
||||
ctx.beginPath()
|
||||
ctx.fillStyle = style.color
|
||||
ctx.arc(pos.x, pos.y, style.size / 2, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
if (canvasStore.isEraser) {
|
||||
const style = getCurrentStrokeStyle()
|
||||
ctx.clearRect(pos.x - style.size / 2, pos.y - style.size / 2, style.size, style.size)
|
||||
} else {
|
||||
const style = getCurrentStrokeStyle()
|
||||
ctx.beginPath()
|
||||
ctx.fillStyle = style.color
|
||||
ctx.arc(pos.x, pos.y, style.size / 2, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -202,8 +223,7 @@ const getImageDataForCat = (): Promise<string> => {
|
|||
// 清空画布
|
||||
const clearCanvas = () => {
|
||||
if (!ctx) return
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.fillRect(0, 0, props.width, props.height)
|
||||
ctx.clearRect(0, 0, props.width, props.height)
|
||||
emit('clear')
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -158,26 +158,30 @@ const loadCats = async (refresh = false) => {
|
|||
const endpoint = activeTab.value === 'global' ? '/cats/global' : '/cats/mine'
|
||||
const page = refresh ? 1 : catsStore.currentPage
|
||||
|
||||
const response = await get<{ cats: Cat[], hasMore: boolean }>(
|
||||
`${endpoint}?page=${page}&limit=20`
|
||||
)
|
||||
if (activeTab.value === 'global') {
|
||||
const response = await get<{ items: Cat[], hasMore: boolean }>(
|
||||
`${endpoint}?page=${page}&limit=20`
|
||||
)
|
||||
|
||||
if (response.success && response.data) {
|
||||
const cats = response.data.cats.map(cat => ({
|
||||
...cat,
|
||||
createdAt: new Date(cat.createdAt)
|
||||
}))
|
||||
|
||||
if (activeTab.value === 'global') {
|
||||
if (response.success && response.data) {
|
||||
const cats = (response.data.items || []).map(cat => ({
|
||||
...cat,
|
||||
createdAt: new Date(cat.createdAt)
|
||||
}))
|
||||
catsStore.setGlobalCats(cats, !refresh)
|
||||
} else {
|
||||
catsStore.setMyCats(cats)
|
||||
catsStore.setHasMore(response.data.hasMore)
|
||||
if (!refresh) catsStore.nextPage()
|
||||
}
|
||||
} else {
|
||||
const response = await get<Cat[]>(`${endpoint}`)
|
||||
|
||||
catsStore.setHasMore(response.data.hasMore)
|
||||
|
||||
if (!refresh) {
|
||||
catsStore.nextPage()
|
||||
if (response.success && response.data) {
|
||||
const cats = (response.data || []).map(cat => ({
|
||||
...cat,
|
||||
createdAt: new Date(cat.createdAt)
|
||||
}))
|
||||
catsStore.setMyCats(cats)
|
||||
catsStore.setHasMore(false)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -286,9 +290,10 @@ const viewDetail = (catId: string) => {
|
|||
// 加载用户收藏
|
||||
const loadFavorites = async () => {
|
||||
try {
|
||||
const response = await get<{ favorites: { catId: string }[] }>('/favorites')
|
||||
const response = await get<Cat[]>('/favorites')
|
||||
if (response.success && response.data) {
|
||||
favoritedIds.value = new Set(response.data.favorites.map(f => f.catId))
|
||||
const cats = Array.isArray(response.data) ? response.data : []
|
||||
favoritedIds.value = new Set(cats.map(c => (c as any)._id || c.id))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load favorites:', error)
|
||||
|
|
@ -297,17 +302,7 @@ const loadFavorites = async () => {
|
|||
|
||||
// 加载用户投票历史
|
||||
const loadVoteHistory = async () => {
|
||||
try {
|
||||
const response = await get<{ votes: { catId: string, type: 'like' | 'dislike' }[] }>('/votes/mine')
|
||||
if (response.success && response.data) {
|
||||
response.data.votes.forEach(v => {
|
||||
voteStatus.value.set(v.catId, v.type)
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
// 投票历史接口可能不存在,忽略错误
|
||||
console.log('Vote history not available')
|
||||
}
|
||||
// 服务端暂无投票历史查询接口,跳过
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
|
|
|||
|
|
@ -142,6 +142,15 @@ const clearCanvas = () => {
|
|||
const handleSubmit = () => {
|
||||
if (!canSubmit.value) return
|
||||
|
||||
// 未登录时跳转到个人页登录
|
||||
if (!userStore.isLoggedIn) {
|
||||
uni.showToast({ title: '请先登录', icon: 'none' })
|
||||
setTimeout(() => {
|
||||
uni.switchTab({ url: '/pages/profile/profile' })
|
||||
}, 1000)
|
||||
return
|
||||
}
|
||||
|
||||
// 提供触觉反馈(需求 10.1)
|
||||
hapticFeedback('medium')
|
||||
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { get, put, post } from '@/utils/api'
|
||||
import { get, put, post, setToken, clearToken } from '@/utils/api'
|
||||
import { hapticFeedback, showSuccess, showError, showLoading, hideLoading } from '@/utils/feedback'
|
||||
import { showErrorToast } from '@/utils/error-handler'
|
||||
|
||||
|
|
@ -175,16 +175,18 @@ const handleLogin = async () => {
|
|||
|
||||
// 调用后端登录 API
|
||||
const response = await post<{
|
||||
openId: string
|
||||
nickname: string
|
||||
avatarUrl: string
|
||||
totalCats: number
|
||||
totalLikes: number
|
||||
totalFavorites: number
|
||||
token: string
|
||||
user: {
|
||||
openId: string
|
||||
nickname: string
|
||||
avatarUrl: string
|
||||
}
|
||||
}>('/auth/login', { code: loginResult.code })
|
||||
|
||||
if (response.success && response.data) {
|
||||
userStore.setUser(response.data)
|
||||
// 保存 JWT token
|
||||
setToken(response.data.token)
|
||||
userStore.setUser(response.data.user)
|
||||
hideLoading()
|
||||
showSuccess('登录成功')
|
||||
}
|
||||
|
|
@ -311,6 +313,7 @@ const handleLogout = () => {
|
|||
confirmColor: '#ff4444',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
clearToken()
|
||||
userStore.logout()
|
||||
uni.showToast({ title: '已退出登录', icon: 'none' })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,8 +49,6 @@ export async function analyzeImage(imageData: string): Promise<RecognitionResult
|
|||
base64Data = await fileToBase64(imageData)
|
||||
}
|
||||
|
||||
console.log('[ai-recognition] sending imageData, length:', base64Data.length, 'starts with:', base64Data.substring(0, 30))
|
||||
|
||||
const response = await post<RecognitionResult>('/recognition', {
|
||||
imageData: base64Data
|
||||
})
|
||||
|
|
@ -89,7 +87,6 @@ async function fileToBase64(filePath: string): Promise<string> {
|
|||
encoding: 'base64',
|
||||
success: (res: any) => {
|
||||
const base64 = typeof res.data === 'string' ? res.data : ''
|
||||
console.log('[ai-recognition] base64 length:', base64.length)
|
||||
resolve(`data:image/png;base64,${base64}`)
|
||||
},
|
||||
fail: (err: any) => {
|
||||
|
|
|
|||
|
|
@ -191,8 +191,8 @@ describe('Unit Tests: Recognition Service', () => {
|
|||
expect(result.suggestion).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should set isValid to false when similarity < 60', () => {
|
||||
const result = createRecognitionResult(59.9)
|
||||
it('should set isValid to false when similarity < 30', () => {
|
||||
const result = createRecognitionResult(29.9)
|
||||
expect(result.isValid).toBe(false)
|
||||
expect(result.suggestion).toBe(RECOGNITION_CONFIG.LOW_SIMILARITY_SUGGESTION)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -402,22 +402,33 @@ async function analyzeImageFeatures(imageData: string): Promise<number> {
|
|||
|
||||
const buf = Buffer.from(base64, 'base64')
|
||||
|
||||
// 获取 64x64 灰度图用于特征分析
|
||||
// 获取 64x64 RGB 图用于特征分析(保留颜色信息)
|
||||
const pixelBuf = await sharp(buf)
|
||||
.resize(64, 64, { fit: 'contain', background: { r: 255, g: 255, b: 255, alpha: 1 } })
|
||||
.grayscale()
|
||||
.removeAlpha()
|
||||
.raw()
|
||||
.toBuffer()
|
||||
|
||||
const size = 64
|
||||
let score = 0
|
||||
|
||||
// 辅助函数:检测像素是否为笔画(非白色)
|
||||
// RGB 三通道中任一通道偏离白色即视为笔画
|
||||
const isStroke = (idx: number): boolean => {
|
||||
const r = pixelBuf[idx * 3]
|
||||
const g = pixelBuf[idx * 3 + 1]
|
||||
const b = pixelBuf[idx * 3 + 2]
|
||||
// 任一通道 < 230 就算有颜色
|
||||
return r < 230 || g < 230 || b < 230
|
||||
}
|
||||
|
||||
// 1. 笔画密度(有画东西就给分)
|
||||
let darkPixels = 0
|
||||
for (let i = 0; i < pixelBuf.length; i++) {
|
||||
if (pixelBuf[i] < 200) darkPixels++
|
||||
const totalPixels = size * size
|
||||
for (let i = 0; i < totalPixels; i++) {
|
||||
if (isStroke(i)) darkPixels++
|
||||
}
|
||||
const density = darkPixels / (size * size)
|
||||
const density = darkPixels / totalPixels
|
||||
// 密度在 5%-40% 之间最像手绘(太少=没画,太多=涂满了)
|
||||
if (density >= 0.05 && density <= 0.5) {
|
||||
score += Math.min(20, density * 100)
|
||||
|
|
@ -429,30 +440,30 @@ async function analyzeImageFeatures(imageData: string): Promise<number> {
|
|||
let topHalf = 0
|
||||
for (let y = 0; y < size / 2; y++) {
|
||||
for (let x = 0; x < size; x++) {
|
||||
if (pixelBuf[y * size + x] < 200) topHalf++
|
||||
if (isStroke(y * size + x)) topHalf++
|
||||
}
|
||||
}
|
||||
const topDensity = topHalf / (size * size / 2)
|
||||
const topDensity = topHalf / (totalPixels / 2)
|
||||
if (topDensity > 0.02) score += 10
|
||||
|
||||
// 3. 下半部分有内容(猫身/腿)
|
||||
let bottomHalf = 0
|
||||
for (let y = size / 2; y < size; y++) {
|
||||
for (let x = 0; x < size; x++) {
|
||||
if (pixelBuf[y * size + x] < 200) bottomHalf++
|
||||
if (isStroke(y * size + x)) bottomHalf++
|
||||
}
|
||||
}
|
||||
const bottomDensity = bottomHalf / (size * size / 2)
|
||||
const bottomDensity = bottomHalf / (totalPixels / 2)
|
||||
if (bottomDensity > 0.02) score += 10
|
||||
|
||||
// 4. 左右都有内容(完整的身体)
|
||||
let leftHalf = 0, rightHalf = 0
|
||||
for (let y = 0; y < size; y++) {
|
||||
for (let x = 0; x < size / 2; x++) {
|
||||
if (pixelBuf[y * size + x] < 200) leftHalf++
|
||||
if (isStroke(y * size + x)) leftHalf++
|
||||
}
|
||||
for (let x = size / 2; x < size; x++) {
|
||||
if (pixelBuf[y * size + x] < 200) rightHalf++
|
||||
if (isStroke(y * size + x)) rightHalf++
|
||||
}
|
||||
}
|
||||
if (leftHalf > 0 && rightHalf > 0) score += 10
|
||||
|
|
@ -461,13 +472,13 @@ async function analyzeImageFeatures(imageData: string): Promise<number> {
|
|||
let transitions = 0
|
||||
for (let y = 0; y < size; y++) {
|
||||
for (let x = 1; x < size; x++) {
|
||||
const prev = pixelBuf[y * size + x - 1] < 200
|
||||
const curr = pixelBuf[y * size + x] < 200
|
||||
const prev = isStroke(y * size + x - 1)
|
||||
const curr = isStroke(y * size + x)
|
||||
if (prev !== curr) transitions++
|
||||
}
|
||||
}
|
||||
// 猫的轮廓应该有适度的复杂度
|
||||
const complexity = transitions / (size * size)
|
||||
const complexity = transitions / totalPixels
|
||||
if (complexity > 0.05) score += 15
|
||||
if (complexity > 0.1) score += 10
|
||||
|
||||
|
|
@ -478,7 +489,7 @@ async function analyzeImageFeatures(imageData: string): Promise<number> {
|
|||
for (let y = 0; y < size; y += 4) {
|
||||
let rowStart = -1
|
||||
for (let x = 0; x < size; x++) {
|
||||
if (pixelBuf[y * size + x] < 200) {
|
||||
if (isStroke(y * size + x)) {
|
||||
rowStart = x
|
||||
break
|
||||
}
|
||||
|
|
|
|||
|
|
@ -132,8 +132,8 @@ describe('Property Tests: Validation Functions', () => {
|
|||
* 对于任意相似度分数,当分数>=60%时按钮应激活,当分数<60%时按钮应禁用。
|
||||
*/
|
||||
describe('Property 7: 相似度阈值按钮状态', () => {
|
||||
it('should activate button when similarity >= 60', () => {
|
||||
const aboveThresholdArb = fc.double({ min: 60, max: 100, noNaN: true })
|
||||
it('should activate button when similarity >= 30', () => {
|
||||
const aboveThresholdArb = fc.double({ min: 30, max: 100, noNaN: true })
|
||||
|
||||
fc.assert(
|
||||
fc.property(aboveThresholdArb, (similarity) => {
|
||||
|
|
@ -143,8 +143,8 @@ describe('Property Tests: Validation Functions', () => {
|
|||
)
|
||||
})
|
||||
|
||||
it('should disable button when similarity < 60', () => {
|
||||
const belowThresholdArb = fc.double({ min: 0, max: 59.99999, noNaN: true })
|
||||
it('should disable button when similarity < 30', () => {
|
||||
const belowThresholdArb = fc.double({ min: 0, max: 29.99999, noNaN: true })
|
||||
|
||||
fc.assert(
|
||||
fc.property(belowThresholdArb, (similarity) => {
|
||||
|
|
@ -154,10 +154,10 @@ describe('Property Tests: Validation Functions', () => {
|
|||
)
|
||||
})
|
||||
|
||||
it('should handle exact threshold value (60)', () => {
|
||||
expect(isSimilarityAboveThreshold(60)).toBe(true)
|
||||
expect(isSimilarityAboveThreshold(59.9)).toBe(false)
|
||||
expect(isSimilarityAboveThreshold(60.1)).toBe(true)
|
||||
it('should handle exact threshold value (30)', () => {
|
||||
expect(isSimilarityAboveThreshold(30)).toBe(true)
|
||||
expect(isSimilarityAboveThreshold(29.9)).toBe(false)
|
||||
expect(isSimilarityAboveThreshold(30.1)).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -231,16 +231,16 @@ describe('Unit Tests: Validation Functions', () => {
|
|||
})
|
||||
|
||||
describe('isSimilarityAboveThreshold - Boundary Values', () => {
|
||||
it('should return false for 59.9%', () => {
|
||||
expect(isSimilarityAboveThreshold(59.9)).toBe(false)
|
||||
it('should return false for 29.9%', () => {
|
||||
expect(isSimilarityAboveThreshold(29.9)).toBe(false)
|
||||
})
|
||||
|
||||
it('should return true for 60%', () => {
|
||||
expect(isSimilarityAboveThreshold(60)).toBe(true)
|
||||
it('should return true for 30%', () => {
|
||||
expect(isSimilarityAboveThreshold(30)).toBe(true)
|
||||
})
|
||||
|
||||
it('should return true for 60.1%', () => {
|
||||
expect(isSimilarityAboveThreshold(60.1)).toBe(true)
|
||||
it('should return true for 30.1%', () => {
|
||||
expect(isSimilarityAboveThreshold(30.1)).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false for 0%', () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user