app: I like setup!

This commit is contained in:
LittleChest 2025-09-27 10:17:42 +08:00
parent 56aa260e60
commit c33999d684

View File

@ -8,56 +8,29 @@
<v-card-title class="justify-center">年龄验证</v-card-title> <v-card-title class="justify-center">年龄验证</v-card-title>
<v-card-text> <v-card-text>
<div class="flex justify-center text-center"> <div class="flex justify-center text-center">
<video <video ref="video" autoplay playsinline muted width="320" height="240" class="rounded-lg bg-black"
ref="video" v-show="cameraActive && !photoData"></video>
autoplay
playsinline
muted
width="320"
height="240"
class="rounded-lg bg-black"
v-show="cameraActive && !photoData"
></video>
<img <img v-if="photoData" :src="photoData" alt="预览" width="320" height="240"
v-if="photoData" class="rounded-lg object-cover" />
:src="photoData"
alt="预览"
width="320"
height="240"
class="rounded-lg object-cover"
/>
</div> </div>
<div v-if="errorMsg" class="text-red-600 text-center mt-2"> <div v-if="errorMsg" class="text-red-600 text-center mt-2">
{{ errorMsg }} {{ errorMsg }}
</div> </div>
<div class="mt-4 flex gap-3 justify-center"> <div class="mt-4 flex gap-3 justify-center items-center">
<v-btn color="primary" @click="takePhoto" :disabled="!cameraActive" v-if="!photoData" <v-btn color="primary" @click="takePhoto" :disabled="!cameraActive" v-if="!photoData">拍照</v-btn>
>拍照</v-btn
>
<v-btn color="secondary" @click="retake" v-if="photoData">重拍</v-btn> <v-btn color="secondary" @click="retake" v-if="photoData">重拍</v-btn>
<v-btn color="success" @click="uploadPhoto" :disabled="!photoBlob">上传</v-btn> <v-btn color="success" @click="uploadPhoto" :disabled="!photoBlob">上传</v-btn>
<label> <label>
<input <input type="file" accept="image/*" @change="onFileChange" class="hidden" ref="fileInput" />
type="file"
accept="image/*"
@change="onFileChange"
class="hidden"
ref="fileInput"
/>
<v-btn color="info" @click="$refs.fileInput.click()">从相册选择</v-btn> <v-btn color="info" @click="$refs.fileInput.click()">从相册选择</v-btn>
</label> </label>
</div> </div>
<v-switch v-model="saveConsent" inset hide-details color="primary" class="mt-4" label="允许 littlew.top 将此信息与你的账户关联" /> <v-progress-linear :indeterminate="true" color="primary" v-if="loading"
class="mt-4"></v-progress-linear>
<v-progress-linear
:indeterminate="true"
v-if="loading"
class="mt-4"
></v-progress-linear>
<div v-if="debug" class="mt-4"> <div v-if="debug" class="mt-4">
<div>调试信息</div> <div>调试信息</div>
@ -68,6 +41,9 @@
<div>年龄{{ ageDisplay }}</div> <div>年龄{{ ageDisplay }}</div>
<div>性别{{ genderDisplay }}</div> <div>性别{{ genderDisplay }}</div>
</div> </div>
<v-switch v-model="saveConsent" color="primary" hide-details class="mt-4"
label="允许littlew.top将此信息与你的账户关联" />
</v-card-text> </v-card-text>
</v-card> </v-card>
</v-col> </v-col>
@ -77,140 +53,138 @@
</v-app> </v-app>
</template> </template>
<script> <script setup>
export default { import { ref, computed, nextTick, onMounted, onBeforeUnmount } from 'vue'
data() {
return { const cameraActive = ref(false)
cameraActive: false, const stream = ref(null)
stream: null, const photoData = ref(null)
photoData: null, const photoBlob = ref(null)
photoBlob: null, const loading = ref(false)
loading: false, const debug = ref('')
debug: '', const errorMsg = ref('')
errorMsg: '', const saveConsent = ref(true)
saveConsent: true, const age = ref(null)
age: null, const gender = ref(null)
gender: null,
} const video = ref(null)
},
methods: { async function startCamera() {
async startCamera() {
try { try {
this.stream = await navigator.mediaDevices.getUserMedia({ errorMsg.value = ''
video: { facingMode: 'user' }, stream.value = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'user' }, audio: false })
audio: false, cameraActive.value = true
}) await nextTick()
this.cameraActive = true const videoEl = video.value
await this.$nextTick() videoEl.srcObject = stream.value
const videoEl = this.$refs.video
videoEl.srcObject = this.stream
await videoEl.play().catch(() => { }) await videoEl.play().catch(() => { })
} catch (e) { } catch (e) {
this.errorMsg = e errorMsg.value = e
} }
}, }
takePhoto() {
function stopCamera() {
if (stream.value) {
stream.value.getTracks().forEach((t) => t.stop())
stream.value = null
}
cameraActive.value = false
}
function takePhoto() {
try { try {
const video = this.$refs.video errorMsg.value = ''
const videoEl = video.value
age.value = null
gender.value = null
const canvas = document.createElement('canvas') const canvas = document.createElement('canvas')
canvas.width = video.videoWidth || 320 canvas.width = videoEl.videoWidth || 320
canvas.height = video.videoHeight || 240 canvas.height = videoEl.videoHeight || 240
const ctx = canvas.getContext('2d') const ctx = canvas.getContext('2d')
ctx.drawImage(video, 0, 0, canvas.width, canvas.height) ctx.drawImage(videoEl, 0, 0, canvas.width, canvas.height)
canvas.toBlob( canvas.toBlob((blob) => {
(blob) => { photoBlob.value = blob
this.photoBlob = blob photoData.value = URL.createObjectURL(blob)
this.photoData = URL.createObjectURL(blob) }, 'image/jpeg', 0.9)
}, stopCamera()
'image/jpeg',
0.9
)
this.stopCamera()
} catch (e) { } catch (e) {
this.errorMsg = e errorMsg.value = e
} }
}, }
retake() {
function retake() {
try { try {
if (this.photoData) URL.revokeObjectURL(this.photoData) if (photoData.value) URL.revokeObjectURL(photoData.value)
this.photoData = null photoData.value = null
this.photoBlob = null photoBlob.value = null
this.startCamera() errorMsg.value = ''
age.value = null
gender.value = null
startCamera()
} catch (e) { } catch (e) {
this.errorMsg = e errorMsg.value = e
} }
},
stopCamera() {
if (this.stream) {
this.stream.getTracks().forEach((t) => t.stop())
this.stream = null
} }
this.cameraActive = false
}, function onFileChange(e) {
onFileChange(e) {
try { try {
errorMsg.value = ''
const file = e.target.files && e.target.files[0] const file = e.target.files && e.target.files[0]
this.photoBlob = file if (!file) throw new Error('未选择文件')
this.photoData = URL.createObjectURL(file) photoBlob.value = file
photoData.value = URL.createObjectURL(file)
age.value = null
gender.value = null
} catch (e) { } catch (e) {
this.errorMsg = e errorMsg.value = e
} }
}, }
async uploadPhoto() {
if (!this.photoBlob) { async function uploadPhoto() {
this.errorMsg = '请先拍照或选择图片' if (!photoBlob.value) {
errorMsg.value = '请先拍照或选择图片'
return return
} }
this.loading = true loading.value = true
this.debug = '' errorMsg.value = ''
debug.value = ''
try { try {
const form = new FormData() const form = new FormData()
form.append('face', this.photoBlob, 'selfie.jpg') form.append('face', photoBlob.value, 'selfie.jpg')
let url = 'https://api.littlew.top/age' let url = 'https://api.littlew.top/age'
if (this.saveConsent) url += '?save=true' if (saveConsent.value) url += '?save=true'
const resp = await fetch(url, { const resp = await fetch(url, { method: 'POST', body: form }, { credentials: 'include' })
method: 'POST',
body: form,
}, {
credentials: 'include',
})
const text = await resp.text() const text = await resp.text()
try { try {
const data = JSON.parse(text) const data = JSON.parse(text)
this.debug = data.raw debug.value = data.raw || ''
this.age = typeof data.age === 'number' ? data.age : -1 age.value = typeof data.age === 'number' ? data.age : -1
this.gender = typeof data.gender === 'number' ? data.gender : -1 gender.value = typeof data.gender === 'number' ? data.gender : -1
} catch (e) { } catch (err) {
this.debug = text debug.value = text
this.age = -1 age.value = -1
this.gender = -1 gender.value = -1
} }
} catch (e) { } catch (e) {
this.errorMsg = e errorMsg.value = e
} finally { } finally {
this.loading = false loading.value = false
} }
}, }
},
computed: { const ageDisplay = computed(() => {
ageDisplay() { if (age.value === null) return '未知'
if (this.age === null) return '未知' return age.value > -1 ? String(age.value) : '未知'
return this.age > -1 ? String(this.age) : '未知' })
},
genderDisplay() { const genderDisplay = computed(() => {
if (this.gender === null) return '未知' if (gender.value === null) return '未知'
if (this.gender === 0) return 'Female' if (gender.value === 0) return 'Female'
if (this.gender === 1) return 'Male' if (gender.value === 1) return 'Male'
return '未知' return '未知'
}, })
},
beforeUnmount() { onBeforeUnmount(() => stopCamera())
this.stopCamera() onMounted(() => startCamera())
},
mounted() {
this.startCamera()
}
}
</script> </script>