mirror of
https://github.com/LittleChest/Age.git
synced 2026-05-06 22:34:48 +08:00
app: Init
This commit is contained in:
commit
6e8180db31
5
.editorconfig
Normal file
5
.editorconfig
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
[*.{js,jsx,ts,tsx,vue}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
* text=auto eol=lf
|
||||||
22
.gitignore
vendored
Normal file
22
.gitignore
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/dist
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
6
.prettierrc.json
Normal file
6
.prettierrc.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/prettierrc",
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 100
|
||||||
|
}
|
||||||
17
eslint.config.js
Normal file
17
eslint.config.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||||
|
import js from '@eslint/js'
|
||||||
|
import pluginVue from 'eslint-plugin-vue'
|
||||||
|
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
{
|
||||||
|
name: 'app/files-to-lint',
|
||||||
|
files: ['**/*.{js,mjs,jsx,vue}'],
|
||||||
|
},
|
||||||
|
|
||||||
|
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
|
||||||
|
|
||||||
|
js.configs.recommended,
|
||||||
|
...pluginVue.configs['flat/essential'],
|
||||||
|
skipFormatting,
|
||||||
|
])
|
||||||
12
index.html
Normal file
12
index.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>年龄验证</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
13
jsconfig.json
Normal file
13
jsconfig.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"target": "es5",
|
||||||
|
"module": "esnext",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
},
|
||||||
|
"lib": ["esnext", "dom", "dom.iterable", "scripthost"]
|
||||||
|
}
|
||||||
|
}
|
||||||
20
package.json
Normal file
20
package.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "age",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@mdi/font": "^7.4.47",
|
||||||
|
"sass-embedded": "^1.93.2",
|
||||||
|
"vue": "^3.5.21",
|
||||||
|
"vuetify": "^3.10.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
|
"vite": "^7.1.5",
|
||||||
|
"vite-plugin-vuetify": "^2.1.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
1407
pnpm-lock.yaml
generated
Normal file
1407
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
163
src/App.vue
Normal file
163
src/App.vue
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
<template>
|
||||||
|
<v-app>
|
||||||
|
<v-main>
|
||||||
|
<v-container class="fill-height" fluid>
|
||||||
|
<v-row align="center" justify="center">
|
||||||
|
<v-col cols="12" sm="8" md="6">
|
||||||
|
<v-card class="pa-6" elevation="6">
|
||||||
|
<v-card-title class="justify-center">年龄验证</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<div class="flex justify-center text-center">
|
||||||
|
<video
|
||||||
|
ref="video"
|
||||||
|
autoplay
|
||||||
|
playsinline
|
||||||
|
muted
|
||||||
|
width="320"
|
||||||
|
height="240"
|
||||||
|
style="border-radius: 8px; background: #000"
|
||||||
|
v-show="cameraActive && !photoData"
|
||||||
|
></video>
|
||||||
|
|
||||||
|
<img
|
||||||
|
v-if="photoData"
|
||||||
|
:src="photoData"
|
||||||
|
alt="自拍预览"
|
||||||
|
width="320"
|
||||||
|
height="240"
|
||||||
|
style="border-radius: 8px; object-fit: cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 flex gap-3 justify-center">
|
||||||
|
<v-btn color="primary" @click="startCamera" v-if="!cameraActive"
|
||||||
|
>打开摄像头</v-btn
|
||||||
|
>
|
||||||
|
<v-btn color="primary" @click="takePhoto" v-if="cameraActive && !photoData"
|
||||||
|
>拍照</v-btn
|
||||||
|
>
|
||||||
|
<v-btn color="secondary" @click="retake" v-if="photoData">重拍</v-btn>
|
||||||
|
<v-btn color="success" @click="uploadPhoto" :disabled="!photoBlob">上传</v-btn>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
@change="onFileChange"
|
||||||
|
class="hidden"
|
||||||
|
ref="fileInput"
|
||||||
|
/>
|
||||||
|
<v-btn color="info" @click="$refs.fileInput.click()">从相册选择</v-btn>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-progress-linear
|
||||||
|
:indeterminate="true"
|
||||||
|
v-if="loading"
|
||||||
|
class="mt-4"
|
||||||
|
></v-progress-linear>
|
||||||
|
|
||||||
|
<div v-if="responseText" class="mt-4">
|
||||||
|
<div>调试信息</div>
|
||||||
|
<pre class="bg-gray-100 p-3 rounded overflow-auto">{{ responseText }}</pre>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
</v-main>
|
||||||
|
</v-app>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
cameraActive: false,
|
||||||
|
stream: null,
|
||||||
|
photoData: null,
|
||||||
|
photoBlob: null,
|
||||||
|
loading: false,
|
||||||
|
responseText: '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async startCamera() {
|
||||||
|
this.stream = await navigator.mediaDevices.getUserMedia({
|
||||||
|
video: { facingMode: 'user' },
|
||||||
|
audio: false,
|
||||||
|
})
|
||||||
|
this.cameraActive = true
|
||||||
|
await this.$nextTick()
|
||||||
|
const videoEl = this.$refs.video
|
||||||
|
videoEl.srcObject = this.stream
|
||||||
|
await videoEl.play().catch(() => {})
|
||||||
|
},
|
||||||
|
takePhoto() {
|
||||||
|
const video = this.$refs.video
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
canvas.width = video.videoWidth || 320
|
||||||
|
canvas.height = video.videoHeight || 240
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
|
||||||
|
canvas.toBlob(
|
||||||
|
(blob) => {
|
||||||
|
this.photoBlob = blob
|
||||||
|
this.photoData = URL.createObjectURL(blob)
|
||||||
|
},
|
||||||
|
'image/jpeg',
|
||||||
|
0.9
|
||||||
|
)
|
||||||
|
this.stopCamera()
|
||||||
|
},
|
||||||
|
retake() {
|
||||||
|
if (this.photoData) URL.revokeObjectURL(this.photoData)
|
||||||
|
this.photoData = null
|
||||||
|
this.photoBlob = null
|
||||||
|
this.startCamera()
|
||||||
|
},
|
||||||
|
stopCamera() {
|
||||||
|
if (this.stream) {
|
||||||
|
this.stream.getTracks().forEach((t) => t.stop())
|
||||||
|
this.stream = null
|
||||||
|
}
|
||||||
|
this.cameraActive = false
|
||||||
|
},
|
||||||
|
onFileChange(e) {
|
||||||
|
const file = e.target.files && e.target.files[0]
|
||||||
|
if (!file) return
|
||||||
|
this.photoBlob = file
|
||||||
|
this.photoData = URL.createObjectURL(file)
|
||||||
|
},
|
||||||
|
async uploadPhoto() {
|
||||||
|
if (!this.photoBlob) return alert('请先拍照或选择图片')
|
||||||
|
this.loading = true
|
||||||
|
this.responseText = ''
|
||||||
|
try {
|
||||||
|
const form = new FormData()
|
||||||
|
form.append('face', this.photoBlob, 'selfie.jpg')
|
||||||
|
|
||||||
|
const resp = await fetch('https://api.littlew.top/age', {
|
||||||
|
method: 'POST',
|
||||||
|
body: form,
|
||||||
|
})
|
||||||
|
|
||||||
|
const text = await resp.text()
|
||||||
|
try {
|
||||||
|
const obj = JSON.parse(text)
|
||||||
|
this.responseText = JSON.stringify(obj, null, 2)
|
||||||
|
} catch (e) {
|
||||||
|
this.responseText = text
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.responseText = e.message
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
this.stopCamera()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
19
src/main.js
Normal file
19
src/main.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import '@mdi/font/css/materialdesignicons.css'
|
||||||
|
import 'vuetify/styles'
|
||||||
|
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
import { createVuetify } from 'vuetify'
|
||||||
|
|
||||||
|
import App from './App.vue'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
app.use(
|
||||||
|
createVuetify({
|
||||||
|
theme: {
|
||||||
|
defaultTheme: 'system',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
app.mount('#app')
|
||||||
23
vite.config.mjs
Normal file
23
vite.config.mjs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import Vue from '@vitejs/plugin-vue'
|
||||||
|
import Vuetify, { transformAssetUrls } from 'vite-plugin-vuetify'
|
||||||
|
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
Vue({
|
||||||
|
template: { transformAssetUrls },
|
||||||
|
}),
|
||||||
|
Vuetify(),
|
||||||
|
],
|
||||||
|
optimizeDeps: {
|
||||||
|
exclude: ['vuetify'],
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('src', import.meta.url)),
|
||||||
|
},
|
||||||
|
extensions: ['.js', '.json', '.jsx', '.mjs', '.ts', '.tsx', '.vue'],
|
||||||
|
},
|
||||||
|
})
|
||||||
Loading…
x
Reference in New Issue
Block a user