mirror of
https://github.com/LittleChest/Age.git
synced 2026-05-06 22:34:48 +08:00
app: I like setup!
This commit is contained in:
parent
56aa260e60
commit
c33999d684
258
src/App.vue
258
src/App.vue
@ -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>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user