feat: 新增新婚送福活动前端页面及功能实现

实现新婚送福活动前端页面,包括以下功能:
1. 活动介绍页展示
2. OCR识别结婚证功能
3. 短信验证码发送与校验
4. 结婚证信息自动填充
5. 表单提交与状态提示
6. 响应式设计与UI组件

新增相关API服务与类型定义,包括活动信息获取、OCR识别、短信验证等接口
This commit is contained in:
前端小啊白 2025-11-14 18:55:27 +08:00
parent 3137b1d7ff
commit 0ba15bd9db
33 changed files with 3288 additions and 52 deletions

76
.gitignore vendored
View File

@ -1,52 +1,24 @@
# Dependencies
node_modules/
# Build outputs
unpackage/
*.local
# Editor directories and files
.vscode/
.idea/
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Coverage directory
coverage/
# Cache
.eslintcache
.stylelintcache
# Temporary files
*.tmp
*.temp
.cache/
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

6
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,6 @@
{
"recommendations": [
"Vue.volar",
"oxc.vscode-oxc"
]
}

5
README.md Normal file
View File

@ -0,0 +1,5 @@
# Vue 3 + TypeScript + Vite
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>1</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

26
package.json Normal file
View File

@ -0,0 +1,26 @@
{
"name": "1",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.17",
"axios": "^1.13.2",
"tailwindcss": "^4.1.17",
"vue": "^3.5.24",
"vue-router": "^4.6.3"
},
"devDependencies": {
"@types/node": "^24.10.0",
"@vitejs/plugin-vue": "^6.0.1",
"@vue/tsconfig": "^0.8.1",
"typescript": "~5.9.3",
"vite": "^7.2.2",
"vue-tsc": "^3.1.3"
}
}

1479
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

BIN
public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

1
public/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
public/ziti.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

BIN
public/头图.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 KiB

BIN
public/扫描.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
public/爱心点缀1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 910 B

BIN
public/爱心点缀2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 886 B

7
src/App.vue Normal file
View File

@ -0,0 +1,7 @@
<script setup lang="ts">
import { RouterView } from 'vue-router'
</script>
<template>
<RouterView />
</template>

1
src/assets/vue.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

View File

@ -0,0 +1,293 @@
<template>
<div class="image-viewer-container w-full h-full">
<!-- 图片错误状态 -->
<div v-if="hasError" class="image-error" :style="containerStyle">
<div class="error-icon"></div>
<span class="error-text">{{ errorText }}</span>
</div>
<!-- 缩略图 -->
<div v-else @click="openViewer">
<img :src="src" :alt="alt || '图片'" :style="{ ...containerStyle, cursor: preview ? 'pointer' : 'default' }"
@load="handleLoad" @error="handleError" />
</div>
<!-- 预览模态框 -->
<transition name="modal">
<div v-if="isViewerOpen" class="viewer-overlay" @click="closeViewer">
<div class="viewer-container" @click.stop>
<!-- 关闭按钮 -->
<button class="close-btn" @click="closeViewer" aria-label="关闭">×</button>
<!-- 预览图片 -->
<img :src="src" :alt="alt || '图片预览'" class="preview-image" :style="{ cursor: zoom ? 'grab' : 'default' }" />
</div>
</div>
</transition>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
// Props
interface Props {
src: string //
alt?: string //
width?: string | number //
height?: string | number //
preview?: boolean //
zoom?: boolean //
customClass?: string // CSS
skeleton?: boolean //
errorText?: string //
placeholder?: string //
}
const props = withDefaults(defineProps<Props>(), {
alt: '',
width: 'auto',
height: 'auto',
preview: true,
zoom: false,
customClass: '',
skeleton: false,
errorText: '图片加载失败',
placeholder: ''
})
// Emits
const emit = defineEmits<{
load: []
error: [error: Event]
preview: []
close: []
}>()
//
const isViewerOpen = ref(false)
const isLoading = ref(true)
const hasError = ref(false)
const errorEvent = ref<Event | null>(null)
//
const containerStyle = computed(() => {
return {
width: typeof props.width === 'number' ? `${props.width}px` : props.width,
height: typeof props.height === 'number' ? `${props.height}px` : props.height
}
})
//
const handleLoad = (event: Event) => {
isLoading.value = false
hasError.value = false
emit('load')
}
//
const handleError = (event: Event) => {
isLoading.value = false
hasError.value = true
errorEvent.value = event
emit('error', event)
}
//
const openViewer = () => {
if (!props.preview) return
isViewerOpen.value = true
//
document.body.style.overflow = 'hidden'
emit('preview')
}
//
const closeViewer = () => {
isViewerOpen.value = false
//
document.body.style.overflow = ''
emit('close')
}
</script>
<style scoped>
/* 缩略图样式 */
.thumbnail {
max-width: 100%;
max-height: 100%;
object-fit: contain;
transition: transform 0.2s, opacity 0.3s;
opacity: 1;
}
.thumbnail:hover {
transform: scale(1.02);
}
.thumbnail.cursor-pointer {
cursor: pointer;
}
/* 图片加载中状态 */
.image-loading {
display: flex;
align-items: center;
justify-content: center;
background-color: #f0f0f0;
border-radius: 4px;
}
.loading-spinner {
width: 30px;
height: 30px;
border: 3px solid rgba(0, 0, 0, 0.1);
border-top-color: #3b82f6;
border-radius: 50%;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* 图片错误状态 */
.image-error {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #f8f8f8;
border: 1px dashed #e5e7eb;
border-radius: 4px;
color: #6b7280;
padding: 20px;
text-align: center;
}
.error-icon {
font-size: 32px;
margin-bottom: 8px;
}
.error-text {
font-size: 14px;
line-height: 1.5;
}
/* 模态框过渡动画 */
.modal-enter-active,
.modal-leave-active {
transition: opacity 0.3s;
}
.modal-enter-from,
.modal-leave-to {
opacity: 0;
}
/* 预览模态框样式 */
.viewer-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.9);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
animation: fadeIn 0.3s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.viewer-container {
position: relative;
max-width: 95%;
max-height: 95%;
animation: zoomIn 0.3s ease-out;
}
@keyframes zoomIn {
from {
transform: scale(0.9);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
.preview-image {
max-width: 100%;
max-height: 90vh;
object-fit: contain;
border-radius: 8px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
}
.close-btn {
position: absolute;
top: -50px;
right: -20px;
width: 40px;
height: 40px;
border: none;
background-color: rgba(255, 255, 255, 0.15);
color: white;
font-size: 28px;
font-weight: 300;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
}
.close-btn:hover {
background-color: rgba(255, 255, 255, 0.3);
transform: scale(1.1);
}
.close-btn:active {
transform: scale(0.95);
}
/* 响应式设计 */
@media (max-width: 768px) {
.viewer-container {
max-width: 98%;
max-height: 98%;
}
.preview-image {
max-height: 85vh;
}
.close-btn {
top: -40px;
right: -10px;
width: 36px;
height: 36px;
font-size: 24px;
}
}
</style>

213
src/components/Message.vue Normal file
View File

@ -0,0 +1,213 @@
<template>
<Transition name="message-fade">
<div v-show="visible" class="message-container" :class="`message-${type}`" :style="customStyle">
<div class="message-content">{{ message }}</div>
<button v-if="closable" class="message-close" @click="close" aria-label="关闭">
<svg class="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clip-rule="evenodd"></path>
</svg>
</button>
</div>
</Transition>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
// Props
const props = defineProps<{
message: string
type?: 'success' | 'error' | 'warning' | 'info'
duration?: number
showIcon?: boolean
closable?: boolean
customClass?: string
offset?: number
center?: boolean
}>()
// Emits
const emit = defineEmits<{
(e: 'close'): void
}>()
//
const visible = ref(false)
let timer: number | null = null
//
const customStyle = computed(() => {
const style: Record<string, string> = {}
if (props.offset) {
style.marginTop = `${props.offset}px`
}
if (props.center) {
style.textAlign = 'center'
}
return style
})
//
const close = () => {
visible.value = false
clearTimer()
emit('close')
}
const clearTimer = () => {
if (timer) {
clearTimeout(timer)
timer = null
}
}
//
onMounted(() => {
visible.value = true
if (props.duration && props.duration > 0) {
timer = window.setTimeout(() => {
close()
}, props.duration)
}
})
onUnmounted(() => {
clearTimer()
})
</script>
<style scoped>
.message-container {
position: relative;
display: flex;
align-items: center;
min-height: 42px;
padding: 8px 16px;
margin-bottom: 12px;
border-radius: 4px;
background-color: #fff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
max-width: 500px;
word-wrap: break-word;
}
/* 消息类型样式 */
.message-success {
border-left: 4px solid #67C23A;
background-color: #F0F9EB;
}
.message-error {
border-left: 4px solid #F56C6C;
background-color: #FEF0F0;
}
.message-warning {
border-left: 4px solid #E6A23C;
background-color: #FDF6EC;
}
.message-info {
border-left: 4px solid #909399;
background-color: #F4F4F5;
}
/* 图标样式 */
.message-icon {
margin-right: 8px;
flex-shrink: 0;
}
.message-success .message-icon {
color: #67C23A;
}
.message-error .message-icon {
color: #F56C6C;
}
.message-warning .message-icon {
color: #E6A23C;
}
.message-info .message-icon {
color: #909399;
}
.message-success .message-content {
color: #306608;
}
.message-error .message-content {
color: #A60C0C;
}
.message-warning .message-content {
color: #9A5518;
}
.message-info .message-content {
color: #606266;
}
/* 关闭按钮样式 */
.message-close {
flex-shrink: 0;
margin-left: 12px;
padding: 0;
background: transparent;
border: none;
cursor: pointer;
font-size: 16px;
line-height: 1;
opacity: 0.5;
transition: opacity 0.3s;
color: inherit;
}
.message-close:hover {
opacity: 1;
}
/* 过渡动画 */
.message-fade-enter-active,
.message-fade-leave-active {
transition: opacity 0.3s ease, transform 0.3s ease;
}
.message-fade-enter-from {
opacity: 0;
transform: translateY(-10px);
}
.message-fade-leave-to {
opacity: 0;
transform: translateY(10px);
}
/* 响应式设计 */
@media (max-width: 768px) {
.message-container {
max-width: 90%;
margin-left: auto;
margin-right: auto;
}
}
/* 鼠标悬停效果 */
.message-container:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
/* 消息容器的wrapper样式 */
:global(.message-container-wrapper) {
pointer-events: none;
}
:global(.message-container-wrapper > div) {
pointer-events: all;
}
</style>

176
src/components/message.ts Normal file
View File

@ -0,0 +1,176 @@
import { createApp, h } from 'vue'
import Message from './Message.vue'
// 消息配置接口
export interface MessageOptions {
message: string
type?: 'success' | 'error' | 'warning' | 'info'
duration?: number
showIcon?: boolean
closable?: boolean
customClass?: string
offset?: number
center?: boolean
onClose?: () => void
}
// 消息实例接口
interface MessageInstance {
close: () => void
}
// 消息容器
const messageContainer = document.createElement('div')
messageContainer.className = 'message-container-wrapper'
document.body.appendChild(messageContainer)
// 默认配置
const defaultOptions: Partial<MessageOptions> = {
duration: 3000,
showIcon: true,
closable: false,
offset: 20,
center: false
}
// 消息管理类
class MessageService {
private instances: MessageInstance[] = []
// 创建消息实例
private createMessage(options: string | MessageOptions): MessageInstance {
// 标准化配置
const finalOptions = typeof options === 'string'
? { message: options, ...defaultOptions }
: { ...defaultOptions, ...options }
// 创建容器元素
const container = document.createElement('div')
messageContainer.appendChild(container)
// 创建应用实例
// 保存对this的引用以避免上下文问题
const self = this
const app = createApp({
render: () => {
return h(Message, {
...finalOptions,
onClose: () => {
self.closeInstance(instance)
finalOptions.onClose?.()
}
})
}
})
// 挂载组件
app.mount(container)
// 计算垂直偏移
const verticalOffset = this.calculateOffset(finalOptions.offset ?? 0)
container.style.cssText = `
position: fixed;
top: ${verticalOffset}px;
left: 50%;
transform: translateX(-50%);
z-index: 9999;
`
// 创建实例对象
const instance: MessageInstance = {
close: () => {
this.closeInstance(instance)
}
}
// 保存实例
this.instances.push(instance)
return instance
}
// 计算垂直偏移
private calculateOffset(baseOffset: number): number {
if (this.instances.length === 0) {
return baseOffset
}
let offset = baseOffset
this.instances.forEach(() => {
// 假设每个消息高度约为60px间距为12px
offset += 60 + 12
})
return offset
}
// 关闭指定实例
private closeInstance(instance: MessageInstance): void {
const index = this.instances.indexOf(instance)
if (index !== -1) {
this.instances.splice(index, 1)
// 重新计算所有消息的位置
this.updateOffsets()
}
}
// 更新所有消息的位置
private updateOffsets(): void {
let offset = defaultOptions.offset || 20
const messageElements = messageContainer.querySelectorAll('div > div')
messageElements.forEach((element, index) => {
if (index < this.instances.length) {
(element.parentElement as HTMLElement).style.top = `${offset}px`
offset += 60 + 12
}
})
}
// 显示普通消息
info(options: string | MessageOptions): MessageInstance {
return this.createMessage(typeof options === 'string'
? { message: options, type: 'info' }
: { ...options, type: 'info' }
)
}
// 显示成功消息
success(options: string | MessageOptions): MessageInstance {
return this.createMessage(typeof options === 'string'
? { message: options, type: 'success' }
: { ...options, type: 'success' }
)
}
// 显示错误消息
error(options: string | MessageOptions): MessageInstance {
return this.createMessage(typeof options === 'string'
? { message: options, type: 'error' }
: { ...options, type: 'error' }
)
}
// 显示警告消息
warning(options: string | MessageOptions): MessageInstance {
return this.createMessage(typeof options === 'string'
? { message: options, type: 'warning' }
: { ...options, type: 'warning' }
)
}
// 关闭所有消息
closeAll(): void {
this.instances.forEach(instance => {
instance.close()
})
this.instances = []
}
}
// 导出消息服务实例
export const message = new MessageService()
// 提供默认导出
export default message

8
src/main.ts Normal file
View File

@ -0,0 +1,8 @@
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(router)
app.mount('#app')

244
src/pages/home/index.vue Normal file
View File

@ -0,0 +1,244 @@
<template>
<div class="min-h-screen bg-[#FCEEF3] overflow-hidden">
<!-- 顶部头图 -->
<div class="w-full overflow-hidden relative">
<img src="/头图.png" alt="活动宣传" class="w-full h-auto" />
<!-- 顶部Logo和标题 -->
<div class="absolute top-0 left-0 py-4 h-full w-full flex flex-col items-start justify-center px-4">
<div class="flex items-start mb-4">
<img src="/logo.png" alt="中国福利彩票" class="h-8 pl-2" />
</div>
<img src="/ziti.png" alt="宁福您彩 新婚送福" class="h-15" />
</div>
</div>
<!-- 表单区域 -->
<div class="p-4 -mt-10 z-10 relative">
<div class="max-w-md mx-auto bg-white rounded-xl shadow-sm p-6">
<!-- 标题 -->
<div class="flex items-center justify-center text-xl text-[#E8424D] mb-6">
<img src="/爱心点缀1.png" alt="" class="h-5">
<span class="px-2">领取专属喜礼</span>
<img src="/爱心点缀2.png" alt="" class="h-5">
</div>
<div>
<!-- 手机号输入 -->
<div class="mb-5 flex items-center gap-3 border border-gray-200 rounded-lg px-4">
<input v-model="formData.phone" type="tel" placeholder="请输入手机号码" maxlength="11"
class="outline-none focus:border-[#E8424D] transition-colors flex-1 h-11" />
<button @click="sendVerificationCode" :disabled="countdown > 0 || formData.phone.length !== 11"
class="h-11 rounded-lg text-sm text-[#E8424D] transition-all disabled:text-[#8a8a8a]">
{{ countdown > 0 ? `${countdown}秒后重发` : '获取验证码' }}
</button>
</div>
<!-- 验证码输入 -->
<div class="mb-5 flex items-center gap-3 border border-gray-200 rounded-lg px-4">
<input v-model="formData.verificationCode" type="text" placeholder="请输入验证码" maxlength="6"
class="outline-none focus:border-[#E8424D] transition-colors flex-1 h-11" />
<button @click="verifyCode" :disabled="formData.verificationCode.length !== 6"
class="h-8 px-4 bg-[#E8424D] text-white rounded-lg text-sm transition-all hover:opacity-90 active:scale-98 disabled:bg-[#8a8a8a]">
验证
</button>
</div>
<!-- 结婚证信息 -->
<div class="mb-6">
<h3 class="text-base text-gray-800 mb-3 font-medium">结婚证信息:</h3>
<div v-if="false" class="border border-gray-200 rounded-lg p-5 flex items-center justify-center">
<div class="relative inline-block" @click="handleScanClick">
<img src="/扫描.png" alt="" class="h-20">
<div
class="h-16 w-26 absolute border border-[#f0f0f0] rounded-md top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 flex justify-center">
<span class="text-[#E2E4E9] text-xs pt-1">结婚证</span>
</div>
<input type="file" ref="ocrUploadId" class="hidden" accept="image/*" @change="handleImageChange" />
</div>
</div>
<div class="w-full flex flex-col items-start border border-gray-200 rounded-lg p-5 bg-[#FAFAFA]">
<div>
<span class="inline-block w-25 text-right">结婚证字号</span>
<span class="pl-2">{{ 11111 }}</span>
</div>
<div class="mt-2">
<span class="inline-block w-25 text-right">男方姓名</span>
<span class="pl-2">{{ 11111 }}</span>
</div>
<div class="mt-2">
<span class="inline-block w-25 text-right">女方姓名</span>
<span class="pl-2">{{ 11111 }}</span>
</div>
</div>
</div>
<!-- 提交按钮 -->
<button @click="submitForm"
class="w-full h-12 bg-gradient-to-r from-[#E8424D] to-[#FF7A7A] text-white rounded-full text-lg font-bold transition-all hover:opacity-90 active:scale-98">
提交领取喜礼
</button>
</div>
<div>
<!-- 活动结束状态 -->
<div class="mt-6 p-4 border border-[#E8424D] bg-[#FFF5F5] rounded-lg text-center">
<div class="text-[#E8424D] text-lg font-semibold mb-2">活动已结束</div>
<div class="text-gray-600 text-sm">感谢您的参与敬请期待下一次活动</div>
<div class="mt-4 text-xs text-gray-500">活动时间2024年1月1日 - 2024年6月30日</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import message from '../../components/message';
import apiService from '../../services/apiService'
import { ref, onMounted } from 'vue'
const ocrUploadId = ref<HTMLInputElement>();
//
const formData = ref({
phone: '',
verificationCode: ''
})
onMounted(() => {
//
apiService.getCurrentActivity().then((response: any) => {
console.log(response);
})
})
//
const countdown = ref(0)
let countdownTimer: number | null = null
//
const sendVerificationCode = () => {
const { phone } = formData.value
//
if (!/^1[3-9]\d{9}$/.test(phone)) {
message.error('请输入正确的手机号码')
return
}
//
apiService.sendSms({
mobile: phone,
type: 0,
}).then(() => {
//
countdown.value = 60
if (countdownTimer) {
clearInterval(countdownTimer)
}
countdownTimer = window.setInterval(() => {
if (countdown.value > 0) {
countdown.value--
} else {
if (countdownTimer) {
clearInterval(countdownTimer)
}
}
}, 1000)
}).catch((error) => {
console.error('发送验证码失败:', error)
message.error('验证码发送失败,请稍后重试')
})
}
//
const verifyCode = () => {
const { verificationCode } = formData.value
if (verificationCode.length !== 6 || !/^\d{6}$/.test(verificationCode)) {
message.error('请输入6位数字验证码')
return
}
//
apiService.login({
mobile: formData.value.phone,
smsCode: verificationCode,
}).then(() => {
message.success('验证码验证成功')
}).catch((error) => {
console.error('验证验证码失败:', error)
message.error('验证码验证失败,请重试')
})
}
//
const handleScanClick = () => {
ocrUploadId.value?.click()
}
const handleImageChange = () => {
const file = ocrUploadId.value?.files?.[0]
if (!file) {
message.error('请选择图片文件')
return
}
// OCR
const formFormData = new FormData()
formFormData.append('file', file)
apiService.uploadOcrImage(formFormData).then((response: any) => {
apiService.sendSms({
mobile: formData.value.phone,
type: 2, // 2 OCR code
}).then((res) => {
apiService.parseOcrInfo({
mobile: formData.value.phone,
smsCode: res.data,
uploadId: response.data.uploadId,
}).then((res) => {
console.log(res);
}).catch((error) => {
console.error('解析OCR信息失败:', error)
message.error('解析OCR信息失败请稍后重试')
})
}).catch((error) => {
console.error('发送验证码失败:', error)
message.error('验证码发送失败,请稍后重试')
})
}).catch((error) => {
console.error('上传OCR图片失败:', error)
message.error('图片上传失败,请稍后重试')
})
}
//
const submitForm = () => {
if (!formData.value.phone || !formData.value.verificationCode) {
message.error('请完善所有信息')
return
}
//
console.log('提交表单数据:', formData.value)
message.success('提交成功,正在处理您的请求...')
}
//
import { onUnmounted } from 'vue'
onUnmounted(() => {
if (countdownTimer) {
clearInterval(countdownTimer)
}
})
</script>

30
src/router/index.ts Normal file
View File

@ -0,0 +1,30 @@
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
// 定义路由配置
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'Home',
component: () => import('../pages/home/index.vue'),
meta: {
title: '首页'
}
}
]
// 创建路由实例
const router = createRouter({
history: createWebHistory(),
routes
})
// 路由前置守卫,用于设置页面标题
router.beforeEach((to, from, next) => {
// 设置页面标题
if (to.meta.title) {
document.title = to.meta.title as string
}
next()
})
export default router

323
src/services/api.md Normal file
View File

@ -0,0 +1,323 @@
# API 文档(领取与兑奖流程)
本文件整理新婚送福活动前端页面所用接口包括领取流程与兑奖页面改造所需的现有接口与拟新增接口。文档基于当前代码库com-marriage-client 与 com-admin-client
## 基本信息
- 服务与端口:
- 领取端com-marriage-client`http://101.35.149.39:8200`,应用名 `nxfc-marriage-client`
- 后台端com-admin-client`http://101.35.149.39:8000`,应用名 `nxfc-admin-client`
- 认证:
- 站点登录与短信验证接口位于 `com-marriage-client``/marriage/common` 路径下。
- 后台活动管理接口位于 `com-admin-client``/admin/activity` 路径下(通常用于管理端)。
---
## 领取流程页面
### 1活动介绍页
用于展示当前活动的名称、时间和面额信息。
- 拟新增(面向领取端,便于前端直接获取展示数据)
- 方法:`GET`
- 路径:`/marriage/activity/current`
- 入参:无
- 出参(示例):
```json
{
"code": 200,
"msg": "",
"data": {
"activityName": "新婚送福",
"activityStartTime": "2025-09-15 00:00:00",
"activityEndTime": "2025-10-31 23:59:59",
"money": 6000,
"status": 1
}
}
```
- 备注:如暂未在领取端提供该接口,可临时通过后台端获取列表接口获得活动信息:
- 方法:`POST`
- 路径:`/admin/activity/list`
- 入参:无
- 出参:`ResultObject` 包含活动集合,字段参考 `MarriageActivity`
### 2OCR识别结婚证自动带出信息领取二维码页
为避免用户手动输入,新增 OCR 流程:上传结婚证图片并识别证件信息,前端使用识别结果自动填充表单,再进行短信校验与领取。
- OCR 上传图片 [新增]
- 方法:`POST`
- 路径:`/marriage/ocr/upload`
- 入参:`multipart/form-data`,字段:`file`(结婚证图片)
- 出参(示例):
```json
{ "code": 200, "msg": "", "data": { "uploadId": "ab12cd34..." } }
```
- 备注:服务端将图片内容以 Base64 缓存于 Redis`OCR_UPLOAD-{uploadId}`,有效期 10 分钟),不落盘文件。
- 发送短信验证码
- 方法:`POST`
- 路径:`/marriage/common/sms`
- 入参:`CommSmsDTO`
- `mobile`:手机号(必填)
- `type`:验证码类型,`0`=登录;`1`=兑换领取;`2`=OCR识别本页使用 `2` [扩展]
- 出参(示例):
```json
{ "code": 200, "msg": "", "data": null }
```
- 备注:当前实现对 `type=1` 的校验逻辑使用了 `code` 字段比对,可能与期望不一致(代码中以 `MarriageCode::getCode == mobile` 判断重复)。如需严格校验手机号唯一性,可调整实现。
- OCR 识别并返回证件信息(供前端自动填充) [新增]
- 方法:`POST`
- 路径:`/marriage/ocr/parse`
- 入参:`OcrParseDTO`
- `mobile`:手机号(必填)
- `smsCode`:短信验证码(必填,取自上方 `/sms``type=2`
- `uploadId`:上传接口返回的标识(必填)
- 出参(示例):
```json
{
"code": 200,
"msg": "",
"data": {
"raw": "<百度OCR原始JSON字符串>",
"words": ["宁夏回族自治区婚姻登记证", "字号6403812025001056", "男方张三", "女方李四", "登记日期2025-10-01"],
"parsed": {
"marriageNo": "6403812025001056",
"husbandName": "张三",
"wifeName": "李四",
"registerDate": "2025-10-01"
}
}
}
```
- 备注:前端可将 `parsed.marriageNo` 用于后续校验与领取,将 `husbandName`/`wifeName`用于展示或校验提示;若识别失败,提示重新上传清晰图片。
- 领取前校验(生成二维码前的校验与预览)
- 方法:`POST`
- 路径:`/marriage/receiveCheck`
- 入参:`MarriageCodeDTO`
- `marriageNo`结婚登记证号必填长度≥11且需符合活动条件
- `receiveName`:领取人姓名(必填)
- `receiveMobile`:领取人手机号(必填)
- `code`:核销码(必填)
- `smsCode`:短信验证码(必填,取自上方 `/sms``type=2`
- `signImage`:领取人电子签名(选填,校验阶段可为空)
- `salesNo`:站点号(选填,用于落库统计)
- 出参:`ResultObject<MarriageCodeVO>`,其中包含可用于二维码展示的 `code` 等字段。
- 业务规则摘要:
- 校验结婚证号格式与活动条件。
- 校验手机号、结婚证号、核销码是否已参与或使用过。
- 校验短信验证码正确性Redis 缓存键:`VERICODE_MOBILE 1-<mobile>`)。
- 确认领取(落库并置为已核销)
- 方法:`POST`
- 路径:`/marriage/receiveCode`
- 入参:`MarriageCodeDTO`
- 与 `receiveCheck` 基本一致,但 `signImage` 必填(签字图片,建议前端以 Base64 上传)
- 出参:`ResultObject`(成功返回 200
- 落库行为:将 `MarriageCode``marriageNo`、`receiveName`、`receiveMobile`、`signImage`、`salesNo`、`receiveMoney=6000`、`receiveTime`、`status=1` 写入并更新。
### 3查看已领取二维码页包括二维码状态
支持按站点和月份查看领取列表统计;也建议提供按核销码查询单条状态的接口。
- 已有:按站点号与月份分页统计列表
- 方法:`POST`
- 路径:`/marriage/codeList`
- 入参:`MarriageCodeDTO`
- `salesNo`:站点号(必填)
- `page`、`size`:分页参数(必填)
- `dataTime`:月份(格式 `yyyy-MM`,可选,默认当前月)
- 出参:`ResultObject<MarriageCodeVO>`
- `codeList`:列表(`MarriageCodeListVO`
- `receiveCount`:数量合计(字符串)
- `receiveAmount`:金额合计(字符串,单位分)
- 拟新增:按核销码查询状态(用于二维码状态页)
- 方法:`POST`
- 路径:`/marriage/code/status`
- 入参:
```json
{ "code": "xxxxxx" }
```
- 出参(示例):
```json
{
"code": 200,
"msg": "",
"data": {
"code": "xxxxxx",
"status": 0,
"marriageNo": null,
"receiveName": null,
"receiveMobile": null,
"receiveTime": null
}
}
```
- 备注:`status=0` 未核销;`status=1` 已核销。已核销时返回对应的领取人信息与时间。
---
## OCR识别流程免手动输入
### 端到端流程
1. 前端上传结婚证图片:`POST /marriage/ocr/upload` [新增] → 返回 `uploadId`
2. 发送短信验证码:`POST /marriage/common/sms``type=2` [扩展]
3. 识别证件信息:`POST /marriage/ocr/parse``mobile`、`smsCode`、`uploadId` [新增] → 返回 `parsed.marriageNo` 等信息
4. 自动填充领取表单,走校验与领取:`POST /marriage/receiveCheck` → `POST /marriage/receiveCode`
### 安全与权限
- `POST /marriage/ocr/**``POST /marriage/common/sms` 已放行匿名访问,用于领取端无登录场景。
- 后端不存储用户上传的原始图片文件,仅在 Redis 缓存 Base6410 分钟),减少存储与泄露风险。
### 配置项标注
- 百度 OCR 相关配置(开发环境) [新增]
- `baidu.ocr.appId`、`baidu.ocr.apiKey`、`baidu.ocr.secretKey`(建议通过环境变量注入)
- `baidu.ocr.authUrl`(默认:`https://aip.baidubce.com/oauth/2.0/token`
- `baidu.ocr.generalUrl`(默认:`https://aip.baidubce.com/rest/2.0/ocr/v1/general_basic`
- `baidu.ocr.storagePath`(当前不落盘,仅占位)
### 测试说明
- 集成测试覆盖 OCR 上传与识别接口,使用本地伪服务模拟百度返回 [新增]
- 测试类:`com-marriage-client/src/test/java/com/jinrui/marriage/client/controller/OcrControllerTest.java`
- 通过属性重定向百度 URL 到本地:`baidu.ocr.authUrl`、`baidu.ocr.generalUrl`
- 断言返回的 `parsed.marriageNo` 包含示例字号片段
---
## 兑奖页面(改为自动带出结婚证号、手机号、姓名)
目标:在兑奖页输入(或扫码)核销码后,自动拉取该码对应的 `marriageNo`、`receiveMobile`、`receiveName` 等信息进行表单预填。
- 拟新增:根据核销码获取领取信息(用于自动带出)
- 方法:`POST`
- 路径:`/marriage/code/info`
- 入参:
```json
{ "code": "xxxxxx" }
```
- 出参(示例):
```json
{
"code": 200,
"msg": "",
"data": {
"code": "xxxxxx",
"status": 1,
"marriageNo": "NX-2025-XXXXXXXXX",
"receiveName": "张三",
"receiveMobile": "13800000000",
"receiveTime": "2025-10-10 12:00:00",
"salesNo": "1001"
}
}
```
- 备注:
- 若未领取(`status=0`),则上述字段为空或不返回;前端可引导先走“领取流程”。
- 若已领取(`status=1`),则用于自动填充兑奖页的表单,减少重复输入。
---
## 站点登录(如需)
用于站点管理后台或业务员端登录。
- 发送登录短信验证码type=0
- 方法:`POST`
- 路径:`/marriage/common/sms`
- 入参:`CommSmsDTO``mobile``type=0`
- 出参:`ResultObject`
- 登录
- 方法:`POST`
- 路径:`/marriage/common/login`
- 入参:`MarrigeLoginDTO`
- `mobile`、`password`(当前固定为 `88888888`)、`smsCode`
- 出参:`ResultObject`(包含登录票据与站点信息)
---
## 返回结构与错误码
- 所有接口统一返回 `ResultObject`
- `code`:业务状态码,`200` 表示成功
- `msg`:提示信息
- `data`:实际数据载体(对象或集合)
- 常见失败信息(摘录):
- `结婚证字号不能为空/长度不对/不符合活动条件`
- `领取人姓名不能为空/领取人手机号不能为空`
- `核验码不能为空/此代金卷不正确/此代金卷已使用过`
- `该领取人已领取过新婚送福活动刮刮乐/这个证号已参与过活动`
- `验证码错误,请重新输入`
---
## 字段模型参考
- `MarriageCodeDTO`
- `marriageNo`、`receiveName`、`code`、`receiveMobile`、`signImage`、`salesNo`、`smsCode`、`dataTime`
- `CommSmsDTO`
- `mobile`、`type``0` 登录,`1` 兑换领取)
- `MarriageCodeVO` / `MarriageCodeListVO`
- 列表与详情视图,包含领取统计与单条领取信息
---
## 前端配合建议
- 二维码内容直接使用核销码 `code` 编码,状态页通过 `code/status` 查询。
- 兑奖页在扫描或输入核销码后,先调用 `code/info` 自动填充,再根据业务需要提交后续操作。
- 如本地开发环境不具备外网资源DB/Redis/微信),建议提供 `local` profile临时屏蔽定时任务与外部依赖仅用于接口联调与页面开发。

141
src/services/apiService.ts Normal file
View File

@ -0,0 +1,141 @@
import request from '@/utils/axios'
// 接口类型定义
export interface ActivityInfo {
activityName: string
activityStartTime: string
activityEndTime: string
money: number
status: number
}
export interface OcrUploadResponse {
uploadId: string
}
export interface OcrParseResponse {
raw: string
words: string[]
parsed: {
marriageNo: string
husbandName: string
wifeName: string
registerDate: string
}
}
export interface MarriageCodeDTO {
marriageNo?: string
receiveName?: string
receiveMobile?: string
code?: string
smsCode?: string
signImage?: string
salesNo?: string
dataTime?: string
}
export interface MarriageCodeVO {
code: string
status: number
marriageNo: string | null
receiveName: string | null
receiveMobile: string | null
receiveTime: string | null
salesNo?: string
}
export interface MarriageCodeListVO {
codeList: MarriageCodeVO[]
receiveCount: string
receiveAmount: string
}
export interface CommSmsDTO {
mobile: string
type: 0 | 1 | 2; // 0=登录1=兑换领取2=OCR识别
}
// API服务实现
export const apiService = {
// 活动介绍页
// 获取当前活动信息
getCurrentActivity() {
return request.get<ActivityInfo>('/marriage/activity/current')
},
// OCR识别相关接口
// 上传OCR图片
uploadOcrImage(formData: FormData) {
return request.post<OcrUploadResponse>('/marriage/ocr/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
},
// 发送短信验证码
sendSms(data: CommSmsDTO) {
return request.post('/marriage/common/sms', data)
},
// OCR识别并返回证件信息
parseOcrInfo(data: {
mobile: string
smsCode: string
uploadId: string
}) {
return request.post<OcrParseResponse>('/marriage/ocr/parse', data)
},
// 领取流程相关接口
// 领取前校验(生成二维码前的校验与预览)
receiveCheck(data: MarriageCodeDTO) {
return request.post<MarriageCodeVO>('/marriage/receiveCheck', data)
},
// 确认领取(落库并置为已核销)
receiveCode(data: MarriageCodeDTO) {
return request.post('/marriage/receiveCode', data)
},
// 二维码状态相关接口
// 按站点号与月份分页统计列表
getCodeList(data: {
salesNo: string
page: number
size: number
dataTime?: string
}) {
return request.post<MarriageCodeListVO>('/marriage/codeList', data)
},
// 按核销码查询状态
getCodeStatus(data: { code: string }) {
return request.post<MarriageCodeVO>('/marriage/code/status', data)
},
// 兑奖页面相关接口
// 根据核销码获取领取信息(用于自动带出)
getCodeInfo(data: { code: string }) {
return request.post<MarriageCodeVO>('/marriage/code/info', data)
},
// 站点登录相关接口
// 登录
login(data: {
mobile: string
password?: string
smsCode?: string
}) {
return request.post('/marriage/common/login', data)
},
// 后台活动管理接口(可选,用于管理端)
// 获取活动列表
getActivityList() {
return request.post('/admin/activity/list')
},
}
export default apiService

1
src/style.css Normal file
View File

@ -0,0 +1 @@
@import "tailwindcss";

116
src/types/apiService.d.ts vendored Normal file
View File

@ -0,0 +1,116 @@
// API服务类型声明
// 接口类型定义
export interface ActivityInfo {
activityName: string
activityStartTime: string
activityEndTime: string
money: number
status: number
}
export interface OcrUploadResponse {
uploadId: string
}
export interface OcrParseResponse {
raw: string
words: string[]
parsed: {
marriageNo: string
husbandName: string
wifeName: string
registerDate: string
}
}
export interface MarriageCodeDTO {
marriageNo?: string
receiveName?: string
receiveMobile?: string
code?: string
smsCode?: string
signImage?: string
salesNo?: string
dataTime?: string
}
export interface MarriageCodeVO {
code: string
status: number
marriageNo: string | null
receiveName: string | null
receiveMobile: string | null
receiveTime: string | null
salesNo?: string
}
export interface MarriageCodeListVO {
codeList: MarriageCodeVO[]
receiveCount: string
receiveAmount: string
}
export interface CommSmsDTO {
mobile: string
type: number // 0=登录1=兑换领取2=OCR识别
}
// API服务接口定义
export interface ApiService {
// 活动介绍页
// 获取当前活动信息
getCurrentActivity(): Promise<any>
// OCR识别相关接口
// 上传OCR图片
uploadOcrImage(formData: FormData): Promise<any>
// 发送短信验证码
sendSms(data: CommSmsDTO): Promise<any>
// OCR识别并返回证件信息
parseOcrInfo(data: {
mobile: string
smsCode: string
uploadId: string
}): Promise<any>
// 领取流程相关接口
// 领取前校验(生成二维码前的校验与预览)
receiveCheck(data: MarriageCodeDTO): Promise<any>
// 确认领取(落库并置为已核销)
receiveCode(data: MarriageCodeDTO): Promise<any>
// 二维码状态相关接口
// 按站点号与月份分页统计列表
getCodeList(data: {
salesNo: string
page: number
size: number
dataTime?: string
}): Promise<any>
// 按核销码查询状态
getCodeStatus(data: { code: string }): Promise<any>
// 兑奖页面相关接口
// 根据核销码获取领取信息(用于自动带出)
getCodeInfo(data: { code: string }): Promise<any>
// 站点登录相关接口
// 登录
login(data: {
mobile: string
password: string
smsCode: string
}): Promise<any>
// 后台活动管理接口(可选,用于管理端)
// 获取活动列表
getActivityList(): Promise<any>
}
export const apiService: ApiService
export default apiService

18
src/types/axios.d.ts vendored Normal file
View File

@ -0,0 +1,18 @@
// 为axios工具添加类型声明
declare module '@/utils/axios' {
import axios from 'axios'
interface CustomAxiosInstance extends axios.AxiosInstance {
get<T = any>(url: string, config?: axios.AxiosRequestConfig): Promise<T>
post<T = any>(url: string, data?: any, config?: axios.AxiosRequestConfig): Promise<T>
put<T = any>(url: string, data?: any, config?: axios.AxiosRequestConfig): Promise<T>
delete<T = any>(url: string, config?: axios.AxiosRequestConfig): Promise<T>
}
const request: CustomAxiosInstance
export default request
export const get: CustomAxiosInstance['get']
export const post: CustomAxiosInstance['post']
export const put: CustomAxiosInstance['put']
export const del: CustomAxiosInstance['delete']
}

74
src/utils/axios.ts Normal file
View File

@ -0,0 +1,74 @@
import axios from 'axios'
// 创建axios实例
const request = axios.create({
baseURL: '/api', // 与Vite代理配置一致
timeout: 10000, // 请求超时时间
headers: {
'Content-Type': 'application/json',
},
})
// 请求拦截器
request.interceptors.request.use(
(config) => {
// 可以在这里添加token等认证信息
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
// 响应拦截器
request.interceptors.response.use(
(response) => {
if(response.data.code == 200){
return response.data;
}else{
return Promise.reject(response.data);
}
},
(error) => {
// 错误处理
console.error('API请求错误:', error)
// 可以根据错误状态码进行不同的处理
if (error.response) {
switch (error.response.status) {
case 401:
// 未授权清除token并跳转到登录页
localStorage.removeItem('token')
// 这里可以添加跳转登录页的逻辑
break
case 403:
alert('没有权限访问该资源')
break
case 404:
alert('请求的资源不存在')
break
case 500:
alert('服务器内部错误')
break
default:
alert(`请求错误: ${error.response.data.message || '未知错误'}`)
}
} else if (error.request) {
// 请求已发出但没有收到响应
alert('网络错误,请检查网络连接')
} else {
// 请求配置出错
alert('请求配置错误')
}
return Promise.reject(error)
}
)
// 导出常用的请求方法
export default request
export const { get, post, put, delete: del } = request

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

9
tailwind.config.js Normal file
View File

@ -0,0 +1,9 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{vue,js,ts}",
],
theme: {},
plugins: [],
}

21
tsconfig.app.json Normal file
View File

@ -0,0 +1,21 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"types": ["vite/client"],
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "src/types/*.d.ts"]
}

7
tsconfig.json Normal file
View File

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

26
tsconfig.node.json Normal file
View File

@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

26
vite.config.ts Normal file
View File

@ -0,0 +1,26 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/
export default defineConfig({
plugins: [vue(), tailwindcss()],
resolve: {
alias: {
'@': '/src',
},
},
server: {
proxy: {
'/api': {
target: 'http://101.35.149.39:8200',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
secure: false,
headers: {
'Access-Control-Allow-Origin': '*',
},
},
},
},
})