init: 你学法我惠农·法润三湘促振兴抽奖项目

This commit is contained in:
lcj
2026-09-03 10:25:51 +08:00
commit 36af48257e
9 changed files with 2723 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
node_modules
dist
*.local
.DS_Store
+12
View 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>
+1162
View File
File diff suppressed because it is too large Load Diff
+18
View File
@@ -0,0 +1,18 @@
{
"name": "nfcj",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^2.7.16"
},
"devDependencies": {
"@vitejs/plugin-vue2": "^2.3.3",
"vite": "^5.4.19"
}
}
+509
View File
@@ -0,0 +1,509 @@
<template>
<div id="app">
<div class="lottery-buttons">
<div class="button-row">
<img
v-for="btn in buttons"
:key="btn.name"
:src="btn.src"
:alt="btn.name"
@click="startLottery(btn.name)"
/>
</div>
<div class="button-row">
<img
v-for="btn in subButtons"
:key="btn.name"
:src="btn.src"
:alt="btn.name"
@click="startLottery(btn.name)"
/>
</div>
</div>
<div v-if="showPasswordDialog" class="dialog-mask" @click.self="closeDialog">
<div class="dialog">
<h3>请输入密码</h3>
<input
v-model="password"
type="password"
placeholder="请输入密码"
@keyup.enter="confirmPassword"
/>
<p v-if="errorMsg" class="dialog-error">{{ errorMsg }}</p>
<div class="dialog-actions">
<button @click="closeDialog">取消</button>
<button @click="confirmPassword">确定</button>
</div>
</div>
</div>
<div v-if="exporting" class="export-loading">导出中...</div>
<div v-if="showDrawingDialog" class="dialog-mask" @click.self="closeDrawingDialog">
<div class="drawing-dialog">
<div class="drawing-text">{{ drawingText }}</div>
<p v-if="apiError && !drawing" class="dialog-error">{{ apiError }}</p>
<div v-else :class="['name-grid', gridClass]">
<span v-for="(n, i) in displayNames" :key="i">{{ n }}</span>
</div>
<div class="drawing-actions">
<button v-if="drawing" @click="stopDrawing">停止</button>
<button v-if="!drawing && pendingBtn === 'ydj'" @click="exportWinners">
导出中奖名单
</button>
<button v-if="!drawing" @click="closeDrawingDialog">关闭</button>
</div>
</div>
</div>
</div>
</template>
<script>
import { NAMES } from './names.js'
const API_BASE = 'https://api.hntgov.com/lylaw/api/lylaw'
export default {
name: 'App',
data() {
return {
buttons: [
{ name: 'ydj', src: 'https://aliyunoss.hn12301.net/learnlaw/nf_ydj.png' },
{ name: 'edj', src: 'https://aliyunoss.hn12301.net/learnlaw/nf_edj.png' },
{ name: 'sdj', src: 'https://aliyunoss.hn12301.net/learnlaw/nf_sdj.png' }
],
subButtons: [
{ name: 'cymd', src: 'https://aliyunoss.hn12301.net/learnlaw/nf_cymd.png' },
{ name: 'zjmd', src: 'https://aliyunoss.hn12301.net/learnlaw/nf_zjmd.png' }
],
prizeNames: {
ydj: '一等奖',
edj: '二等奖',
sdj: '三等奖'
},
prizeLevels: {
ydj: 1,
edj: 2,
sdj: 3
},
prizeCounts: {
ydj: 50,
edj: 200,
sdj: 375
},
showPasswordDialog: false,
password: '',
errorMsg: '',
pendingBtn: '',
showDrawingDialog: false,
exporting: false,
drawingText: '',
drawing: false,
rollingNames: [],
winnerNames: [],
apiWinners: null,
apiError: '',
apiPromise: null,
rollTimer: null
}
},
computed: {
displayNames() {
return this.drawing ? this.rollingNames : this.winnerNames
},
gridClass() {
const len = this.displayNames.length
if (len <= 50) return 'cols-5'
if (len <= 200) return 'cols-10'
return 'cols-15'
}
},
methods: {
startLottery(name) {
if (name === 'cymd') {
this.exportAttendUsers()
return
}
if (name === 'zjmd') {
this.exportWinners()
return
}
this.pendingBtn = name
this.password = ''
this.errorMsg = ''
this.showPasswordDialog = true
},
confirmPassword() {
if (this.password === '123456') {
this.showPasswordDialog = false
const prize = this.prizeNames[this.pendingBtn]
if (prize) this.beginDrawing(prize)
} else {
this.errorMsg = '密码错误'
}
},
closeDialog() {
this.showPasswordDialog = false
},
beginDrawing(prize) {
this.drawingText = prize + '抽取中...'
this.drawing = true
this.rollingNames = []
this.winnerNames = []
this.apiWinners = null
this.apiError = ''
this.showDrawingDialog = true
this.rollNames()
this.rollTimer = setInterval(this.rollNames, 200)
const level = this.prizeLevels[this.pendingBtn]
this.apiPromise = fetch(API_BASE + '/chouJiangWeek?wenjuan_id=1002&level=' + level, {
method: 'POST'
})
.then((r) => r.json())
.then((res) => {
if (res.code === 0 && res.Data && Array.isArray(res.Data.wenjuanWinDetails)) {
this.apiWinners = res.Data.wenjuanWinDetails.map((d) => d.user_name)
} else {
this.apiError = res.msg || '接口请求失败'
}
})
.catch(() => {
this.apiError = '接口请求失败'
})
},
rollNames() {
const count = this.prizeCounts[this.pendingBtn] || 50
const pool = NAMES.slice()
const picked = []
for (let i = 0; i < count; i++) {
picked.push(pool.splice(Math.floor(Math.random() * pool.length), 1)[0])
}
this.rollingNames = picked
},
stopDrawing() {
if (this.rollTimer) {
clearInterval(this.rollTimer)
this.rollTimer = null
}
this.drawing = false
this.drawingText = this.prizeNames[this.pendingBtn] + '中奖名单'
const show = () => {
if (this.apiWinners) this.winnerNames = this.apiWinners
}
if (this.apiWinners || this.apiError) {
show()
} else {
this.apiPromise.then(show)
}
},
closeDrawingDialog() {
if (this.rollTimer) {
clearInterval(this.rollTimer)
this.rollTimer = null
}
this.showDrawingDialog = false
},
requestExport(url) {
this.exporting = true
const call = (method) => fetch(url, { method })
call('POST')
.then((r) => {
if (r.status === 405) return call('GET')
return r
})
.then((r) => r.json())
.then((res) => {
if (res.code === 0 && res.Data) {
const iframe = document.createElement('iframe')
iframe.style.display = 'none'
iframe.src = res.Data.indexOf('http') === 0 ? res.Data : 'https://api.hntgov.com' + res.Data
document.body.appendChild(iframe)
setTimeout(() => iframe.remove(), 60000)
} else {
alert(res.msg || '导出失败')
}
})
.catch(() => alert('导出失败'))
.finally(() => {
this.exporting = false
})
},
exportWinners() {
this.requestExport(API_BASE + '/exportThisTimeWinUserReport?wenjuan_id=1002')
},
exportAttendUsers() {
const d = new Date()
const periodTime =
d.getFullYear() +
'-' +
String(d.getMonth() + 1).padStart(2, '0') +
'-' +
String(d.getDate()).padStart(2, '0')
this.requestExport(
API_BASE + '/exportAttendUserReport?wenjuan_id=1002&period_time=' + periodTime
)
}
}
}
</script>
<style>
@font-face {
font-family: 'ALiMaMa';
src: url('./components/ALiMaMa.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
html,
body {
height: 100%;
margin: 0;
overflow: hidden;
user-select: none;
}
input {
user-select: text;
}
#app {
position: relative;
height: 100vh;
font-family: Avenir, Helvetica, Arial, sans-serif;
text-align: center;
background: url('https://aliyunoss.hn12301.net/learnlaw/nf_cjbj.jpg') no-repeat center center;
background-size: 100% 100%;
overflow: hidden;
}
.lottery-buttons {
position: absolute;
left: 0;
right: 0;
bottom: 16vh;
display: flex;
flex-direction: column;
align-items: center;
gap: 2vh;
}
.button-row {
display: flex;
justify-content: center;
gap: 4vw;
}
.button-row:last-child {
gap: 10vw;
}
.lottery-buttons img {
zoom: 0.5;
cursor: pointer;
user-select: none;
-webkit-user-drag: none;
}
.dialog-mask {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}
.dialog {
width: 320px;
padding: 24px;
background: #fff;
border-radius: 8px;
text-align: center;
}
.dialog h3 {
margin: 0 0 16px;
}
.dialog input {
width: 100%;
box-sizing: border-box;
padding: 8px 12px;
border: 1px solid #ccc;
border-radius: 4px;
outline: none;
caret-color: transparent;
}
.dialog-error {
margin: 8px 0 0;
color: #e53935;
font-size: 14px;
}
.dialog-actions {
margin-top: 20px;
display: flex;
justify-content: center;
gap: 16px;
}
.dialog-actions button {
padding: 6px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.dialog-actions button:first-child {
background: #eee;
}
.dialog-actions button:last-child {
background: #e53935;
color: #fff;
}
.drawing-dialog {
width: 85vw;
padding: 24px 32px;
background: transparent;
color: #fff;
text-align: center;
}
.drawing-text {
display: inline-block;
padding: 12px 56px 16px;
background: linear-gradient(180deg, #55985e 0%, #4a8952 50%, #3f7546 100%);
border: 2px solid rgba(254, 215, 40, 0.85);
border-radius: 14px;
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.25);
font-family: 'ALiMaMa', Avenir, Helvetica, Arial, sans-serif;
font-size: 40px;
font-weight: bold;
letter-spacing: 8px;
color: #fed728;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.45);
}
.name-grid {
margin-top: 16px;
display: grid;
padding: 16px;
background: linear-gradient(180deg, #55985e 0%, #4a8952 50%, #3f7546 100%);
border: 2px solid rgba(255, 255, 255, 0.35);
border-radius: 16px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.25);
overflow: hidden;
}
.name-grid.cols-5 {
grid-template-columns: repeat(5, 1fr);
gap: 10px;
font-size: 16px;
}
.name-grid.cols-10 {
grid-template-columns: repeat(10, 1fr);
gap: 6px;
font-size: 14px;
}
.name-grid.cols-15 {
grid-template-columns: repeat(15, 1fr);
gap: 4px;
font-size: 12px;
}
.name-grid span {
background: #fff;
color: #333;
border-radius: 6px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
padding: 6px 8px;
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.name-grid.cols-10 span {
padding: 4px;
}
.name-grid.cols-15 span {
padding: 2px;
border-radius: 4px;
}
.drawing-actions {
margin-top: 20px;
display: flex;
justify-content: center;
gap: 20px;
}
.drawing-actions button {
padding: 12px 44px;
border: 2px solid rgba(255, 255, 255, 0.6);
border-radius: 999px;
background: linear-gradient(180deg, #ff7043 0%, #e53935 60%, #c62828 100%);
color: #fff;
font-family: 'ALiMaMa', Avenir, Helvetica, Arial, sans-serif;
font-size: 20px;
letter-spacing: 4px;
cursor: pointer;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.35);
transition: transform 0.15s, box-shadow 0.15s, filter 0.15s;
}
.drawing-actions button:hover {
filter: brightness(1.08);
transform: translateY(-1px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.35);
}
.drawing-actions button:active {
transform: translateY(1px);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
}
.export-loading {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
padding: 16px 40px;
background: rgba(0, 0, 0, 0.75);
border-radius: 8px;
color: #fff;
font-size: 18px;
letter-spacing: 2px;
z-index: 200;
}
@media (max-height: 900px) {
.drawing-text {
font-size: 26px;
}
.name-grid.cols-10 {
gap: 4px;
font-size: 12px;
}
.name-grid.cols-10 span {
padding: 2px;
}
.name-grid.cols-15 {
gap: 3px;
font-size: 10px;
}
.name-grid.cols-15 span {
padding: 1px;
}
}
</style>
Binary file not shown.
+6
View File
@@ -0,0 +1,6 @@
import Vue from 'vue'
import App from './App.vue'
new Vue({
render: (h) => h(App)
}).$mount('#app')
+1002
View File
File diff suppressed because it is too large Load Diff
+10
View File
@@ -0,0 +1,10 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue2'
export default defineConfig({
base: '/nfcj/',
plugins: [vue()],
server: {
open: true
}
})