first commit
This commit is contained in:
BIN
__pycache__/main.cpython-312.pyc
Normal file
BIN
__pycache__/main.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/main.cpython-313.pyc
Normal file
BIN
__pycache__/main.cpython-313.pyc
Normal file
Binary file not shown.
307
docker_deply.sh
Executable file
307
docker_deply.sh
Executable file
@@ -0,0 +1,307 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Docker 镜像构建和部署自动化脚本
|
||||||
|
# chmod u+x docker_deply.sh
|
||||||
|
# 用法:
|
||||||
|
# ./docker_deply.sh # 完整构建和部署流程 (默认AMD64架构)
|
||||||
|
# ./docker_deply.sh -amd # 构建和部署AMD64架构
|
||||||
|
# ./docker_deply.sh -arm # 构建和部署ARM64架构
|
||||||
|
# ./docker_deply.sh -upload # 仅上传已存在的tar文件并部署
|
||||||
|
# ./docker_deply.sh -upload -amd # 仅上传已存在的AMD64架构tar文件并部署
|
||||||
|
# ./docker_deply.sh -upload -arm # 仅上传已存在的ARM64架构tar文件并部署
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# 配置非局域网变量 - 公网上传方法
|
||||||
|
# SERVER_HOST="121.43.104.161" # 服务器IP地址
|
||||||
|
# SERVER_USER="ubuntu" # 服务器用户名
|
||||||
|
# SERVER_PASSWORD="qweasdzxc1" # 服务器密码
|
||||||
|
# SERVER_PORT="6222"
|
||||||
|
|
||||||
|
# 配置局域网变量 - 公司局域网上传方法
|
||||||
|
SERVER_HOST="6.6.6.86" # 服务器IP地址
|
||||||
|
SERVER_USER="ubuntu" # 服务器用户名
|
||||||
|
SERVER_PASSWORD="qweasdzxc1" # 服务器密码
|
||||||
|
SERVER_PORT="22" # SSH端口,默认22
|
||||||
|
|
||||||
|
|
||||||
|
IMAGE_NAME="crystal_back" # Docker镜像名称
|
||||||
|
IMAGE_TAG="latest" # Docker镜像标签
|
||||||
|
CONTAINER_NAME="crystal-container" # 容器名称
|
||||||
|
LOCAL_PORT="8011" # 本地端口
|
||||||
|
CONTAINER_PORT="8011" # 容器端口
|
||||||
|
TAR_FILE="${IMAGE_NAME}-${IMAGE_TAG}.tar" # 压缩包文件名
|
||||||
|
|
||||||
|
# 架构相关变量
|
||||||
|
PLATFORM="linux/amd64" # 默认架构
|
||||||
|
ARCH_SUFFIX="" # 架构后缀,用于区分不同架构的tar文件
|
||||||
|
|
||||||
|
# 颜色输出
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# 日志函数
|
||||||
|
log_info() {
|
||||||
|
echo -e "${BLUE}[INFO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_success() {
|
||||||
|
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_warning() {
|
||||||
|
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_error() {
|
||||||
|
echo -e "${RED}[ERROR]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 检查依赖
|
||||||
|
check_dependencies() {
|
||||||
|
log_info "检查依赖..."
|
||||||
|
|
||||||
|
if ! command -v docker &> /dev/null; then
|
||||||
|
log_error "Docker 未安装,请先安装 Docker"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v sshpass &> /dev/null; then
|
||||||
|
log_error "sshpass 未安装,请先安装 sshpass"
|
||||||
|
log_info "macOS: brew install sshpass"
|
||||||
|
log_info "Ubuntu: sudo apt-get install sshpass"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "依赖检查完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 解析命令行参数
|
||||||
|
parse_arguments() {
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
-amd)
|
||||||
|
PLATFORM="linux/amd64"
|
||||||
|
ARCH_SUFFIX="-amd64"
|
||||||
|
log_info "设置目标架构为 AMD64"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-arm)
|
||||||
|
PLATFORM="linux/arm64"
|
||||||
|
ARCH_SUFFIX="-arm64"
|
||||||
|
log_info "设置目标架构为 ARM64"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-upload)
|
||||||
|
UPLOAD_ONLY=true
|
||||||
|
log_info "设置为仅上传模式"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
log_error "未知参数: $1"
|
||||||
|
log_info "支持的参数: -amd, -arm, -upload"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# 更新TAR_FILE名,包含架构后缀
|
||||||
|
TAR_FILE="${IMAGE_NAME}-${IMAGE_TAG}${ARCH_SUFFIX}.tar"
|
||||||
|
log_info "镜像文件名: ${TAR_FILE}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 构建Docker镜像
|
||||||
|
build_image() {
|
||||||
|
log_info "开始构建 Docker 镜像..."
|
||||||
|
|
||||||
|
# 检查是否存在旧的tar文件
|
||||||
|
if [ -f "$TAR_FILE" ]; then
|
||||||
|
log_warning "发现旧的tar文件,正在删除..."
|
||||||
|
rm -f "$TAR_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 构建镜像并导出为tar文件
|
||||||
|
docker buildx build --platform $PLATFORM -t "${IMAGE_NAME}:${IMAGE_TAG}" --output type=docker,dest="./${TAR_FILE}" .
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
log_success "Docker 镜像构建完成: ${TAR_FILE}"
|
||||||
|
else
|
||||||
|
log_error "Docker 镜像构建失败"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 上传文件到服务器
|
||||||
|
upload_to_server() {
|
||||||
|
log_info "上传文件到服务器..."
|
||||||
|
|
||||||
|
# 上传镜像tar文件
|
||||||
|
sshpass -p "$SERVER_PASSWORD" scp -P "$SERVER_PORT" -o StrictHostKeyChecking=no "$TAR_FILE" "${SERVER_USER}@${SERVER_HOST}:/tmp/"
|
||||||
|
|
||||||
|
# 上传.env文件
|
||||||
|
sshpass -p "$SERVER_PASSWORD" scp -P "$SERVER_PORT" -o StrictHostKeyChecking=no ".env" "${SERVER_USER}@${SERVER_HOST}:/tmp/"
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
log_success "文件上传成功"
|
||||||
|
else
|
||||||
|
log_error "文件上传失败"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 在服务器上部署
|
||||||
|
deploy_on_server() {
|
||||||
|
log_info "在服务器上部署..."
|
||||||
|
|
||||||
|
sshpass -p "$SERVER_PASSWORD" ssh -p "$SERVER_PORT" -o StrictHostKeyChecking=no "${SERVER_USER}@${SERVER_HOST}" << EOF
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "[INFO] 开始服务器端部署..."
|
||||||
|
|
||||||
|
# 检查并停止现有容器
|
||||||
|
if sudo docker ps -a --format 'table {{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
|
||||||
|
echo "[INFO] 发现现有容器 ${CONTAINER_NAME},正在停止并删除..."
|
||||||
|
sudo docker stop ${CONTAINER_NAME} || true
|
||||||
|
sudo docker rm ${CONTAINER_NAME} || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 检查并删除现有镜像
|
||||||
|
if sudo docker images --format 'table {{.Repository}}:{{.Tag}}' | grep -q "^${IMAGE_NAME}:${IMAGE_TAG}$"; then
|
||||||
|
echo "[INFO] 发现现有镜像 ${IMAGE_NAME}:${IMAGE_TAG},正在删除..."
|
||||||
|
sudo docker rmi ${IMAGE_NAME}:${IMAGE_TAG} || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 加载新镜像
|
||||||
|
echo "[INFO] 加载新镜像..."
|
||||||
|
sudo docker load -i /tmp/${TAR_FILE}
|
||||||
|
|
||||||
|
# 验证镜像是否加载成功
|
||||||
|
if sudo docker images | grep -q "${IMAGE_NAME}"; then
|
||||||
|
echo "[SUCCESS] 镜像加载成功"
|
||||||
|
else
|
||||||
|
echo "[ERROR] 镜像加载失败"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 创建并使用.env文件中的变量
|
||||||
|
echo "[INFO] 启动新容器..."
|
||||||
|
sudo docker run -d -p ${LOCAL_PORT}:${CONTAINER_PORT} --name ${CONTAINER_NAME} \
|
||||||
|
--env-file /tmp/.env \
|
||||||
|
${IMAGE_NAME}:${IMAGE_TAG}
|
||||||
|
|
||||||
|
# 验证容器是否启动成功
|
||||||
|
if sudo docker ps | grep -q "${CONTAINER_NAME}"; then
|
||||||
|
echo "[SUCCESS] 容器启动成功"
|
||||||
|
echo "[INFO] 容器状态:"
|
||||||
|
sudo docker ps | grep "${CONTAINER_NAME}"
|
||||||
|
else
|
||||||
|
echo "[ERROR] 容器启动失败"
|
||||||
|
echo "[INFO] 查看容器日志:"
|
||||||
|
sudo docker logs ${CONTAINER_NAME}
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 清理临时文件
|
||||||
|
echo "[INFO] 清理临时文件..."
|
||||||
|
rm -f /tmp/${TAR_FILE}
|
||||||
|
rm -f /tmp/.env
|
||||||
|
|
||||||
|
echo "[SUCCESS] 部署完成!"
|
||||||
|
echo "[INFO] 应用访问地址: http://${SERVER_HOST}:${LOCAL_PORT}"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
log_success "服务器部署完成"
|
||||||
|
else
|
||||||
|
log_error "服务器部署失败"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 清理本地文件
|
||||||
|
cleanup_local() {
|
||||||
|
log_info "清理本地临时文件..."
|
||||||
|
|
||||||
|
if [ -f "$TAR_FILE" ]; then
|
||||||
|
rm -f "$TAR_FILE"
|
||||||
|
log_success "本地临时文件已清理"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 显示部署信息
|
||||||
|
show_deployment_info() {
|
||||||
|
log_success "部署完成!"
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo "部署信息:"
|
||||||
|
echo "=========================================="
|
||||||
|
echo "服务器地址: ${SERVER_HOST}"
|
||||||
|
echo "应用端口: ${LOCAL_PORT}"
|
||||||
|
echo "访问地址: http://${SERVER_HOST}:${LOCAL_PORT}"
|
||||||
|
echo "容器名称: ${CONTAINER_NAME}"
|
||||||
|
echo "镜像名称: ${IMAGE_NAME}:${IMAGE_TAG}"
|
||||||
|
echo "目标架构: ${PLATFORM}"
|
||||||
|
echo "镜像文件: ${TAR_FILE}"
|
||||||
|
echo "=========================================="
|
||||||
|
echo ""
|
||||||
|
log_info "如需查看容器日志,请在服务器上运行: sudo docker logs ${CONTAINER_NAME}"
|
||||||
|
log_info "如需停止容器,请在服务器上运行: sudo docker stop ${CONTAINER_NAME}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 主函数
|
||||||
|
main() {
|
||||||
|
echo "=========================================="
|
||||||
|
echo "Docker 镜像构建和部署自动化脚本"
|
||||||
|
echo "=========================================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 解析命令行参数
|
||||||
|
UPLOAD_ONLY=false
|
||||||
|
parse_arguments "$@"
|
||||||
|
|
||||||
|
# 检查配置
|
||||||
|
if [ "$SERVER_HOST" = "your-server-ip" ] || [ "$SERVER_PASSWORD" = "your-password" ]; then
|
||||||
|
log_error "请先配置脚本顶部的服务器信息"
|
||||||
|
log_info "需要修改的变量:"
|
||||||
|
log_info " - SERVER_HOST: 服务器IP地址"
|
||||||
|
log_info " - SERVER_USER: 服务器用户名"
|
||||||
|
log_info " - SERVER_PASSWORD: 服务器密码"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 检查是否是上传模式
|
||||||
|
if [ "$UPLOAD_ONLY" = true ]; then
|
||||||
|
log_info "检测到 -upload 参数,跳过构建步骤"
|
||||||
|
|
||||||
|
# 检查tar文件是否存在
|
||||||
|
if [ ! -f "$TAR_FILE" ]; then
|
||||||
|
log_error "未找到tar文件: $TAR_FILE"
|
||||||
|
log_info "请先运行脚本构建镜像,或确保tar文件存在"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "找到tar文件: $TAR_FILE"
|
||||||
|
|
||||||
|
# 执行上传和部署流程
|
||||||
|
upload_to_server
|
||||||
|
deploy_on_server
|
||||||
|
cleanup_local
|
||||||
|
show_deployment_info
|
||||||
|
else
|
||||||
|
# 执行完整的部署流程
|
||||||
|
check_dependencies
|
||||||
|
build_image
|
||||||
|
upload_to_server
|
||||||
|
deploy_on_server
|
||||||
|
cleanup_local
|
||||||
|
show_deployment_info
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 脚本入口
|
||||||
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||||
|
main "$@"
|
||||||
|
fi
|
||||||
182
gsdh.csv
Normal file
182
gsdh.csv
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
new_id,name,industry_company,contact_info,phone,fee,payment_channel,is_signed
|
||||||
|
1,孙成耕,"高等教育,农业",,13708714964,26,一见星球,FALSE
|
||||||
|
2,和德光,冶炼自动化,,18088930670,26,一见星球,FALSE
|
||||||
|
3,熊志华,大数据,,13922200602,26,一见星球,FALSE
|
||||||
|
4,和兆云,工业自动化,,13987660503,26,一见星球,FALSE
|
||||||
|
5,程云龙,航空,,18669209165,26,一见星球,FALSE
|
||||||
|
6,唐佑初,家电、百货、农产品,,18988138899,26,一见星球,FALSE
|
||||||
|
7,杨婵,教育,,18687715696,26,一见星球,FALSE
|
||||||
|
8,姜安峰,文化传媒,,13211747473,26,一见星球,FALSE
|
||||||
|
9,刘夏,教育,,18487147072,26,一见星球,FALSE
|
||||||
|
10,王为家,酒水经销商,,15198989711,26,一见星球,FALSE
|
||||||
|
11,胡泰姜,互联网,,18608719363,26,一见星球,FALSE
|
||||||
|
12,杨丽萍,医药,,15969182571,26,一见星球,FALSE
|
||||||
|
13,林七,保险,,15024589748,26,一见星球,FALSE
|
||||||
|
14,Zoe,教育+农业,,15969500332,26,一见星球,FALSE
|
||||||
|
15,李怡清,软件,,13888947017,26,一见星球,FALSE
|
||||||
|
16,朱英杰,文化传播,,15087063865,26,一见星球,FALSE
|
||||||
|
17,Johnny,跨境电商,,18687605779,26,微信,FALSE
|
||||||
|
18,张妩,服务(酒店),,1388829509,26,微信,FALSE
|
||||||
|
19,马建军,大溪谷 瓶装水、,,13987672775,26,微信,FALSE
|
||||||
|
20,周海洋,制造业,,17608785235,26,微信,FALSE
|
||||||
|
21,云天明,财务管理,,18866433121,26,微信,FALSE
|
||||||
|
22,文昱桦,数字化转型,,13777018667,26,微信,FALSE
|
||||||
|
23,文昱桦朋友,数字化转型,,13777018667,26,微信,FALSE
|
||||||
|
24,龙金贵,凯缘春酒业,,13698779505,26,微信,FALSE
|
||||||
|
25,唐儒,大健康轻创业,,13099992576,26,微信,FALSE
|
||||||
|
26,窦海涛,人工智能,,13845432196,26,微信,FALSE
|
||||||
|
27,刘磊,跨境电子商务,,13529005525,26,微信,FALSE
|
||||||
|
28,李若菡,道医,有事,已退费,14787451514,26,微信,FALSE
|
||||||
|
29,戴军,个体,,13529383555,26,微信,FALSE
|
||||||
|
30,李延辉,生物医药,,18908789027,26,微信,FALSE
|
||||||
|
31,张鹏程,医疗机器人,,13308789694,26,微信,FALSE
|
||||||
|
32,倪钜,珠宝,,13150587227,26,微信,FALSE
|
||||||
|
33,陈昭宇,文旅,免费,17600099614,,,FALSE
|
||||||
|
34,张馨艺,自媒体,免费,13888651626,,,FALSE
|
||||||
|
35,黄升,社群运营,免费,1,,,FALSE
|
||||||
|
36,马艺,AI跨境,免费,,,,FALSE
|
||||||
|
37,岳岳,自媒体,,18687329243,志愿者,,FALSE
|
||||||
|
38,岳岳朋友刘致豪,自媒体,,15188137009,志愿者,,FALSE
|
||||||
|
39,岳岳王天胜,自媒体,,13320543169,志愿者,,FALSE
|
||||||
|
40,岳岳宗彗莲,自媒体,,19988427837,志愿者,,FALSE
|
||||||
|
41,陆起,飞书,免费,13347705547,,,FALSE
|
||||||
|
42,陆起朋友王维,飞书,免费,13521170687,,,FALSE
|
||||||
|
43,陆起朋友,飞书,免费,,,,FALSE
|
||||||
|
44,刘翔,财务公司,,18725032602,,,FALSE
|
||||||
|
45,宋宇,后端工程师,,18388411803,,,FALSE
|
||||||
|
46,高立仁,新能源汽车公司,,19187151953,,,FALSE
|
||||||
|
47,赵若曦,云纺集团,免费,13987673203,,,FALSE
|
||||||
|
48,李李,智行滇峰说演讲,免费,,,,FALSE
|
||||||
|
49,海浪,安信集团,免费,17713249555,,,FALSE
|
||||||
|
50,姚超,思享界,,13033031987,志愿者,,FALSE
|
||||||
|
51,姚超摄影师,思享界,,,摄影师,,FALSE
|
||||||
|
52,小唯,,,,志愿者,,FALSE
|
||||||
|
53,朱晓丽,律师,,18206868114,志愿者,,FALSE
|
||||||
|
54,涞涞,康养中医药,,13089899853,志愿者,,FALSE
|
||||||
|
55,赖应海,创业者,,18157390046,志愿者,,FALSE
|
||||||
|
56,冯立伟,量迹,,13220881316,总负责,,FALSE
|
||||||
|
57,赵广明,鬼才明创意工作室,,18611218523,总策划,,FALSE
|
||||||
|
58,甘昭泉,量迹,,18585164448,分享嘉宾,,FALSE
|
||||||
|
59,丁靖洋,派动科技,,18221820102,分享嘉宾,,FALSE
|
||||||
|
60,刘伯煜,大筑科技,,18312876673,分享嘉宾,,FALSE
|
||||||
|
61,曹平飞,阿里云,嘉宾,,,,FALSE
|
||||||
|
62,李欢,阿里云,,18187811772,分享嘉宾,,FALSE
|
||||||
|
63,暂定,百度,,,分享嘉宾,,FALSE
|
||||||
|
64,郑文浩,百度,嘉宾,15820798660,,,
|
||||||
|
65,三三,彩创、飞书,,,分享嘉宾,,
|
||||||
|
66,钱丹,彩创、飞书,,18788400115,联合主办,,
|
||||||
|
67,沙梅兰,掌上春城,,15912524519,媒体,,
|
||||||
|
68,李凌竹,广播电视台,,13888585678,媒体,,
|
||||||
|
69,晏丽,云南广电传媒,,15911520520,分享嘉宾,,
|
||||||
|
70,拍摄,云南广电传媒,,15911520520,摄影师,,
|
||||||
|
71,采访,云南广电传媒,,15911520520,采访,,
|
||||||
|
72,许应烨,AI,,15331657816,,,
|
||||||
|
73,汤小姐,打杂,已进群,13922791778,,,
|
||||||
|
74,张玮楠,茶企,已有微信,18289790037,,,
|
||||||
|
75,李逸飞,图书电商,嘉宾,18810586550,,,
|
||||||
|
76,Robyn,教育留学,w,13211788556,,,
|
||||||
|
77,刘胜,教育,w,14787812507,,,
|
||||||
|
78,闵莹,软件开发,w,18687510353,,,
|
||||||
|
79,金玉辉,昆明飞威报刊图书有限公司,/,18388109039,,,
|
||||||
|
80,陈佑,图书批发,/,18288927098,,,
|
||||||
|
81,蒋晓伟,法务,w,13085392813,26,微信,
|
||||||
|
82,xiaoMa,IT,,1.32387E+11,,,
|
||||||
|
83,王瑞琦,科技,w,15925183977,,,
|
||||||
|
84,秦贵,图书行业,/,18788443543,,,
|
||||||
|
85,杨慧,图书电商,/,18869769859,,,
|
||||||
|
86,赵榕,图书类目,/,18608899476,,,
|
||||||
|
87,邓勇,教育,w,18208813207,,,
|
||||||
|
88,张出高,教育,w,18206871081,,,
|
||||||
|
89,王海倩,教育,,15559634666,,,
|
||||||
|
90,陈锐嶂,商贸流通,w,13008671498,,,
|
||||||
|
91,雷乔玲,财务,w,15198838759,,,
|
||||||
|
92,lucky,跨境电商,w,18214611725,,,
|
||||||
|
93,李忠岘,教育,w,15808886488,,,
|
||||||
|
94,陈奕恒,跨境电商,w,15887158441,,,
|
||||||
|
95,经纬,科技,,13987382399,,,
|
||||||
|
96,段晓方,开发,w,17687023127,,,
|
||||||
|
97,高立仁,新能源汽车,w,17504311180,,,
|
||||||
|
98,吴松泽,教师,w,18762304432,,,
|
||||||
|
99,周正宜,跨境电商,w,17689845870,,,
|
||||||
|
100,赵云翔,跨境电商,w,17751891232,,,
|
||||||
|
101,李皓月,医疗,w,17368456286,,,
|
||||||
|
102,徐衡,设计,w,13518713214,26,微信,
|
||||||
|
103,葛一瑶,电力,w,15912137626,26,微信,
|
||||||
|
104,陆易航,智能开发,w,15087148039,26,微信,
|
||||||
|
105,邓智铭,程序员,w,13211685339,,,
|
||||||
|
106,姚来弟,计算机,w,18288440214,,,
|
||||||
|
107,萧清,数字文创,w,17606909560,,,
|
||||||
|
108,发发,新媒体,,13278714977,,,
|
||||||
|
109,塔米,教育,,13250714571,,,
|
||||||
|
110,张华英,律师,w,13658853415,,,
|
||||||
|
111,王天胜,直播带货,,15687698763,,,
|
||||||
|
112,张橙,传媒,w,18082712849,26,微信,
|
||||||
|
113,周总,"商业服务,为企业申办各类型补贴",w,13987141588,,,
|
||||||
|
114,吴潇雪,法律,w,15969591218,,,
|
||||||
|
115,蒋乃浩,娱乐,w,13888862549,26,微信,
|
||||||
|
116,李烨,医疗,w,13619605970,,,
|
||||||
|
117,白羽,文化产业,w,13187856156,,,
|
||||||
|
118,李易为,文旅,,13085358008,,,
|
||||||
|
119,高嘉煜,软件开发,w,15126615549,26,微信,
|
||||||
|
120,毕先生,花卉,w,13888466977,,,
|
||||||
|
121,汤镇宇,互联网,w,13629615669,,,
|
||||||
|
122,刘先生,电商,w,15908809814,,,
|
||||||
|
123,晏瑞婕,金融,w,13013313443,,,
|
||||||
|
124,杨势挥,KTV,w,18787440187,,,
|
||||||
|
125,滕乾忠,教育,,18187128470,,,
|
||||||
|
126,芾邴,"教育,科技",,13161994127,,,
|
||||||
|
127,Alice,云计算,,15087060387,,,
|
||||||
|
128,李丹,法律,w,15125990928,,,
|
||||||
|
129,吴疆,能源,w,13718215039,,,
|
||||||
|
130,高丹妮,房地产、新媒体,免费,15287335911,青商会员,,
|
||||||
|
131,聂怡然,智能餐饮,w,13808725796,,,
|
||||||
|
132,一雄,OPC,w,18812269173,26,微信,
|
||||||
|
133,杜丽娜,自由职业,w,13888115882,,,
|
||||||
|
134,ayao,音乐ai,,18987063951,,,
|
||||||
|
135,周其建,人工智能,,18887103888,,,
|
||||||
|
136,杨万峰,制造业,,1528821438,,,
|
||||||
|
137,Mia,旅居,,19911866777,,,
|
||||||
|
138,张雪娟,教育,,18988153273,,,
|
||||||
|
139,禹姣,银行业,,15887205279,,,
|
||||||
|
140,郑华,零售,,15140295598,,,
|
||||||
|
141,毕玉,AI,,19146959276,,,
|
||||||
|
142,象叔,IP内容营销,,13888834251,,,
|
||||||
|
143,王贺,旅游规划与设计,,13987188550,,,
|
||||||
|
144,瑾瑾瑾,AI创作,,13658897411,,,
|
||||||
|
145,阿布,传统行业,,15087097719,,,
|
||||||
|
146,杨文虎,新闻,,13577034568,,,
|
||||||
|
147,陈,知识产权,,13211662238,,,
|
||||||
|
148,安娜,自由职业,,18208706046,,,
|
||||||
|
149,暴雨,医药,,15969182571,,,
|
||||||
|
150,小源,教育,,18314522519,,,
|
||||||
|
151,杨泽文,电力,,17803866644,,,
|
||||||
|
152,張競丹,旅遊,,18987589587,,,
|
||||||
|
153,赵锐涵,金融,,13529216958,,,
|
||||||
|
154,范洁,大健康食品,,13888182707,,,
|
||||||
|
155,陈莹,服务,,19224081189,,,
|
||||||
|
156,杨立平,漫剧,,13988620996,26,微信,
|
||||||
|
157,杨立蕊,漫剧,,13888707563,26,微信,
|
||||||
|
158,郭艳,茶行业,,18787145995,26,微信,
|
||||||
|
159,孟妹,广告,,13759060777,26,微信,
|
||||||
|
160,丁艳玲,餐饮行业,,18187315565,26,微信,
|
||||||
|
161,钟源,教育/餐饮,,18314522519,26,微信,
|
||||||
|
162,付吟吟,昆明精悦广告有限公司总经理,免费,13619622871,青商会员,,
|
||||||
|
163,李慧,云南智慧光经贸有限公司 法人,免费,15825186018,青商会员,,
|
||||||
|
164,陈锦玥,自由职业,,18987471556,26,微信,
|
||||||
|
165,鄢木西,星梧科技(云南)有限责任公司,,19912828161,26,微信,
|
||||||
|
166,孔浩,IT信息行业,,13668780930,26,微信,
|
||||||
|
167,李拉,互联网,,13759408584,26,微信,
|
||||||
|
168,吴芬,食品电商,,15331725758,26,微信,
|
||||||
|
169,黄政葵,,,,26,微信,
|
||||||
|
170,温立新,村播,免费,13330499193,,,
|
||||||
|
171,孔德风,百度,嘉宾,18087960002,,,
|
||||||
|
172,李程旻,火山引擎,免费,19187149926,,,
|
||||||
|
173,王若谷,咖啡,免费,18468038180,,,
|
||||||
|
174,张童,电力行业,,18187527804,26,微信,
|
||||||
|
175,李虹婷,投资,,18608738555,青商会员,,
|
||||||
|
176,周鑫,银行,,,青商会员,,
|
||||||
|
177,张志刚,青创会,嘉宾,15508805566,,,
|
||||||
|
178,段晓芳,全栈开发工程师,免费,17687023127,,,
|
||||||
|
179,张培恒,富滇银行-oa,免费,18288711024,,,
|
||||||
|
180,王娇,软件运维,免费,15808742554,,,
|
||||||
|
181,宁杰,医疗行业,,13698786789,26,微信,
|
||||||
|
310
main.py
Normal file
310
main.py
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
from fastapi import FastAPI, HTTPException, Request
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
|
from fastapi.responses import HTMLResponse, JSONResponse
|
||||||
|
from pydantic import BaseModel
|
||||||
|
import psycopg2
|
||||||
|
from psycopg2.extras import RealDictCursor
|
||||||
|
from typing import Optional, List, Dict
|
||||||
|
import random
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
# Database connection parameters
|
||||||
|
DB_CONFIG = {
|
||||||
|
"host": "121.43.104.161",
|
||||||
|
"port": "6432",
|
||||||
|
"user": "gsdh",
|
||||||
|
"password": "123gsdh",
|
||||||
|
"database": "gsdh"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Mount static files
|
||||||
|
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||||
|
templates = Jinja2Templates(directory="templates")
|
||||||
|
|
||||||
|
# Models
|
||||||
|
class CheckinRequest(BaseModel):
|
||||||
|
gsdh_id: str
|
||||||
|
name: str
|
||||||
|
phone: str
|
||||||
|
company_name: Optional[str] = None
|
||||||
|
position: Optional[str] = None
|
||||||
|
business_scope: Optional[str] = None
|
||||||
|
vision_2026: Optional[str] = None
|
||||||
|
location: Optional[str] = None
|
||||||
|
|
||||||
|
class AddUserRequest(BaseModel):
|
||||||
|
name: str
|
||||||
|
phone: str
|
||||||
|
industry_company: Optional[str] = None
|
||||||
|
|
||||||
|
def get_db_connection():
|
||||||
|
conn = psycopg2.connect(**DB_CONFIG)
|
||||||
|
return conn
|
||||||
|
|
||||||
|
def assign_seat(cur, user_industry: str) -> str:
|
||||||
|
"""
|
||||||
|
Allocate a seat based on:
|
||||||
|
1. Even distribution (13 tables, max 12 per table)
|
||||||
|
2. Mix industries (try to put user in a table where their industry is least represented)
|
||||||
|
"""
|
||||||
|
TOTAL_TABLES = 13
|
||||||
|
MAX_PER_TABLE = 12
|
||||||
|
|
||||||
|
# Initialize table stats
|
||||||
|
# tables = { 1: {'count': 0, 'industries': []}, ... }
|
||||||
|
tables = {i: {'count': 0, 'industries': []} for i in range(1, TOTAL_TABLES + 1)}
|
||||||
|
|
||||||
|
# Fetch current seating status
|
||||||
|
# We join checkin_info with gsdh_data to get industries of people ALREADY SEATED
|
||||||
|
query = """
|
||||||
|
SELECT ci.location, gd.industry_company
|
||||||
|
FROM checkin_info ci
|
||||||
|
JOIN gsdh_data gd ON ci.gsdh_id = gd.new_id
|
||||||
|
WHERE ci.location IS NOT NULL AND ci.location LIKE '第%桌'
|
||||||
|
"""
|
||||||
|
cur.execute(query)
|
||||||
|
rows = cur.fetchall()
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
loc = row[0] # e.g. "第1桌"
|
||||||
|
ind = row[1]
|
||||||
|
try:
|
||||||
|
# Extract table number
|
||||||
|
table_num = int(loc.replace("第", "").replace("桌", ""))
|
||||||
|
if 1 <= table_num <= TOTAL_TABLES:
|
||||||
|
tables[table_num]['count'] += 1
|
||||||
|
if ind:
|
||||||
|
tables[table_num]['industries'].append(ind)
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Filter tables that are not full
|
||||||
|
available_tables = [t for t in tables.items() if t[1]['count'] < MAX_PER_TABLE]
|
||||||
|
|
||||||
|
if not available_tables:
|
||||||
|
return "自由席" # Fallback if all full
|
||||||
|
|
||||||
|
# Strategy 1: Find tables with Minimum Count (Even Distribution)
|
||||||
|
min_count = min(t[1]['count'] for t in available_tables)
|
||||||
|
candidates_step1 = [t for t in available_tables if t[1]['count'] == min_count]
|
||||||
|
|
||||||
|
# Strategy 2: Among candidates, find best for diversity
|
||||||
|
# We want a table where user_industry is NOT present, or present least often
|
||||||
|
best_table = None
|
||||||
|
|
||||||
|
# If user has no industry info, just pick random from candidates
|
||||||
|
if not user_industry:
|
||||||
|
best_table = random.choice(candidates_step1)[0]
|
||||||
|
else:
|
||||||
|
# Score candidates: lower score is better (score = count of this industry in that table)
|
||||||
|
scored_candidates = []
|
||||||
|
for table_id, stats in candidates_step1:
|
||||||
|
# Simple fuzzy check: count how many times user_industry appears in stats['industries']
|
||||||
|
# We use simple string containment
|
||||||
|
industry_count = sum(1 for existing_ind in stats['industries'] if existing_ind and user_industry in existing_ind)
|
||||||
|
scored_candidates.append((table_id, industry_count))
|
||||||
|
|
||||||
|
# Sort by industry count (asc)
|
||||||
|
scored_candidates.sort(key=lambda x: x[1])
|
||||||
|
|
||||||
|
# Pick the one with least collision
|
||||||
|
min_collision = scored_candidates[0][1]
|
||||||
|
final_candidates = [x[0] for x in scored_candidates if x[1] == min_collision]
|
||||||
|
best_table = random.choice(final_candidates)
|
||||||
|
|
||||||
|
return f"第{best_table}桌"
|
||||||
|
|
||||||
|
def get_tablemates(cur, location: str, exclude_id: str) -> List[Dict]:
|
||||||
|
"""
|
||||||
|
Get up to 3 random tablemates from the same table location.
|
||||||
|
"""
|
||||||
|
if not location or location == "自由席":
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Debug: Check who is at this location
|
||||||
|
print(f"DEBUG: Fetching tablemates for location: '{location}', excluding: '{exclude_id}'")
|
||||||
|
|
||||||
|
# Important: Ensure the location string format matches database exactly
|
||||||
|
# Database seems to store "第X桌", ensuring consistent querying
|
||||||
|
|
||||||
|
# Updated query to fetch more details from checkin_info
|
||||||
|
query = """
|
||||||
|
SELECT ci.name, gd.industry_company, ci.company_name, ci.position, ci.business_scope, ci.vision_2026
|
||||||
|
FROM checkin_info ci
|
||||||
|
LEFT JOIN gsdh_data gd ON ci.gsdh_id = gd.new_id
|
||||||
|
WHERE ci.location = %s AND ci.gsdh_id != %s
|
||||||
|
ORDER BY RANDOM()
|
||||||
|
LIMIT 3
|
||||||
|
"""
|
||||||
|
cur.execute(query, (location, exclude_id))
|
||||||
|
rows = cur.fetchall()
|
||||||
|
|
||||||
|
print(f"DEBUG: Found {len(rows)} tablemates")
|
||||||
|
|
||||||
|
tablemates = []
|
||||||
|
for row in rows:
|
||||||
|
tablemates.append({
|
||||||
|
"name": row[0],
|
||||||
|
"industry": row[1] or "暂无行业信息",
|
||||||
|
"company_name": row[2] or "暂无单位信息",
|
||||||
|
"position": row[3] or "暂无职务信息",
|
||||||
|
"business_scope": row[4] or "暂无业务信息",
|
||||||
|
"vision_2026": row[5] or "暂无愿景信息"
|
||||||
|
})
|
||||||
|
return tablemates
|
||||||
|
|
||||||
|
@app.get("/", response_class=HTMLResponse)
|
||||||
|
async def read_root(request: Request):
|
||||||
|
return templates.TemplateResponse("index.html", {"request": request})
|
||||||
|
|
||||||
|
@app.get("/api/search")
|
||||||
|
async def search_user(query: str):
|
||||||
|
"""
|
||||||
|
Search user by phone (exact match) or name (fuzzy match).
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
conn = get_db_connection()
|
||||||
|
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||||
|
|
||||||
|
# Priority 1: Exact Phone Match
|
||||||
|
cur.execute("SELECT * FROM gsdh_data WHERE phone = %s", (query,))
|
||||||
|
user = cur.fetchone()
|
||||||
|
|
||||||
|
# Priority 2: Fuzzy Name Match if not found by phone
|
||||||
|
if not user:
|
||||||
|
# Using ILIKE for case-insensitive fuzzy search
|
||||||
|
cur.execute("SELECT * FROM gsdh_data WHERE name ILIKE %s", (f"%{query}%",))
|
||||||
|
users = cur.fetchall()
|
||||||
|
|
||||||
|
if len(users) == 0:
|
||||||
|
conn.close()
|
||||||
|
return JSONResponse(content={"found": False, "message": "未查询到相关信息,请检查输入是否正确"}, status_code=404)
|
||||||
|
elif len(users) > 1:
|
||||||
|
# If multiple users found by name, return list for user to select (simplified here to return first or error)
|
||||||
|
# For this MVP, let's return all matching users so frontend can handle selection
|
||||||
|
conn.close()
|
||||||
|
return JSONResponse(content={"found": True, "multiple": True, "users": users})
|
||||||
|
else:
|
||||||
|
user = users[0]
|
||||||
|
|
||||||
|
# Check if already signed
|
||||||
|
if user.get('is_signed') == 'TRUE':
|
||||||
|
# If already signed, fetch their assigned seat and tablemates
|
||||||
|
cur.execute("SELECT location FROM checkin_info WHERE gsdh_id = %s", (user['new_id'],))
|
||||||
|
checkin_info = cur.fetchone()
|
||||||
|
assigned_seat = checkin_info['location'] if checkin_info else "自由席"
|
||||||
|
|
||||||
|
# Fetch tablemates
|
||||||
|
# Use a new cursor for the helper function to avoid cursor factory conflict or state issues
|
||||||
|
# Note: get_tablemates expects a standard cursor for tuple results, but here we have DictCursor
|
||||||
|
# We can adapt get_tablemates or just use key access if we pass the DictCursor
|
||||||
|
# Let's create a fresh standard cursor to be safe and consistent with get_tablemates implementation
|
||||||
|
cur_plain = conn.cursor()
|
||||||
|
tablemates = get_tablemates(cur_plain, assigned_seat, user['new_id'])
|
||||||
|
cur_plain.close()
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
return JSONResponse(content={
|
||||||
|
"found": True,
|
||||||
|
"user": user,
|
||||||
|
"already_signed": True,
|
||||||
|
"seat": assigned_seat,
|
||||||
|
"tablemates": tablemates
|
||||||
|
})
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
return JSONResponse(content={"found": True, "user": user, "already_signed": False})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return JSONResponse(content={"error": str(e)}, status_code=500)
|
||||||
|
|
||||||
|
@app.post("/api/checkin")
|
||||||
|
async def checkin_user(checkin_data: CheckinRequest):
|
||||||
|
try:
|
||||||
|
conn = get_db_connection()
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
# 0. Get user's industry from gsdh_data to help with seat allocation
|
||||||
|
cur.execute("SELECT industry_company FROM gsdh_data WHERE new_id = %s", (checkin_data.gsdh_id,))
|
||||||
|
res = cur.fetchone()
|
||||||
|
user_industry = res[0] if res else ""
|
||||||
|
|
||||||
|
# 1. Allocate Seat
|
||||||
|
assigned_seat = assign_seat(cur, user_industry)
|
||||||
|
|
||||||
|
# 2. Insert into checkin_info with assigned seat
|
||||||
|
insert_sql = """
|
||||||
|
INSERT INTO checkin_info
|
||||||
|
(name, phone, company_name, position, business_scope, vision_2026, location, gsdh_id)
|
||||||
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
|
||||||
|
"""
|
||||||
|
cur.execute(insert_sql, (
|
||||||
|
checkin_data.name,
|
||||||
|
checkin_data.phone,
|
||||||
|
checkin_data.company_name,
|
||||||
|
checkin_data.position,
|
||||||
|
checkin_data.business_scope,
|
||||||
|
checkin_data.vision_2026,
|
||||||
|
assigned_seat, # Use the generated seat
|
||||||
|
checkin_data.gsdh_id
|
||||||
|
))
|
||||||
|
|
||||||
|
# 3. Update gsdh_data is_signed to TRUE
|
||||||
|
update_sql = "UPDATE gsdh_data SET is_signed = 'TRUE' WHERE new_id = %s"
|
||||||
|
cur.execute(update_sql, (checkin_data.gsdh_id,))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
# 4. Fetch tablemates for the newly assigned seat
|
||||||
|
tablemates = get_tablemates(cur, assigned_seat, checkin_data.gsdh_id)
|
||||||
|
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return {"success": True, "message": "签到成功!", "seat": assigned_seat, "tablemates": tablemates}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if 'conn' in locals():
|
||||||
|
conn.rollback()
|
||||||
|
return JSONResponse(content={"success": False, "message": f"签到失败: {str(e)}"}, status_code=500)
|
||||||
|
|
||||||
|
@app.get("/add-user", response_class=HTMLResponse)
|
||||||
|
async def add_user_page(request: Request):
|
||||||
|
return templates.TemplateResponse("add_user.html", {"request": request})
|
||||||
|
|
||||||
|
@app.post("/api/add-user")
|
||||||
|
async def add_user_api(user_data: AddUserRequest):
|
||||||
|
try:
|
||||||
|
conn = get_db_connection()
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
# Check if phone already exists
|
||||||
|
cur.execute("SELECT * FROM gsdh_data WHERE phone = %s", (user_data.phone,))
|
||||||
|
if cur.fetchone():
|
||||||
|
conn.close()
|
||||||
|
return JSONResponse(content={"success": False, "message": "该手机号已存在"}, status_code=400)
|
||||||
|
|
||||||
|
new_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
insert_sql = """
|
||||||
|
INSERT INTO gsdh_data (new_id, name, phone, industry_company, is_signed)
|
||||||
|
VALUES (%s, %s, %s, %s, 'FALSE')
|
||||||
|
"""
|
||||||
|
cur.execute(insert_sql, (new_id, user_data.name, user_data.phone, user_data.industry_company))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return {"success": True, "message": "添加成功", "new_id": new_id}
|
||||||
|
except Exception as e:
|
||||||
|
if 'conn' in locals():
|
||||||
|
conn.rollback()
|
||||||
|
return JSONResponse(content={"success": False, "message": f"添加失败: {str(e)}"}, status_code=500)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||||
BIN
static/image.jpg
Normal file
BIN
static/image.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 229 KiB |
298
templates/add_user.html
Normal file
298
templates/add_user.html
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>云南AI共生大会 - 添加嘉宾</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--primary-color: #00f2ff; /* Cyan from the AI sphere/text */
|
||||||
|
--secondary-color: #0066ff; /* Rich blue */
|
||||||
|
--bg-color: #050814; /* Deep space blue/black */
|
||||||
|
--card-bg: rgba(12, 24, 50, 0.5); /* Glassy blue tint */
|
||||||
|
--text-color: #ffffff;
|
||||||
|
--text-muted: #b0c4de; /* Light blue-grey */
|
||||||
|
--accent-glow: rgba(0, 242, 255, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
background-image:
|
||||||
|
radial-gradient(circle at 50% 0%, #1a3a75 0%, #050814 60%),
|
||||||
|
radial-gradient(circle at 85% 30%, rgba(0, 242, 255, 0.1) 0%, transparent 40%),
|
||||||
|
radial-gradient(circle at 15% 70%, rgba(0, 102, 255, 0.15) 0%, transparent 40%);
|
||||||
|
background-attachment: fixed;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background:
|
||||||
|
linear-gradient(rgba(255,255,255,0.03) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(255,255,255,0.03) 1px, transparent 1px);
|
||||||
|
background-size: 50px 50px;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
padding: 30px 20px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
border-bottom: 1px solid rgba(0, 242, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-img {
|
||||||
|
width: 100%;
|
||||||
|
max-height: 220px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
box-shadow: 0 0 20px rgba(0, 242, 255, 0.2);
|
||||||
|
border: 1px solid rgba(0, 242, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2.4rem;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
background: linear-gradient(180deg, #ffffff 0%, #d0eaff 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
text-shadow: 0 0 30px rgba(0, 242, 255, 0.4);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: var(--card-bg);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 30px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
border: 1px solid rgba(0, 242, 255, 0.2);
|
||||||
|
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.5), inset 0 0 20px rgba(0, 242, 255, 0.05);
|
||||||
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
input, textarea, select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
border: 1px solid rgba(0, 242, 255, 0.3);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: white;
|
||||||
|
font-size: 1rem;
|
||||||
|
outline: none;
|
||||||
|
transition: all 0.3s;
|
||||||
|
box-shadow: inset 0 2px 5px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus, textarea:focus {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 15px rgba(0, 242, 255, 0.3), inset 0 2px 5px rgba(0,0,0,0.3);
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 16px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: linear-gradient(90deg, #0051ff 0%, #00f2ff 100%);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 242, 255, 0.3);
|
||||||
|
text-shadow: 0 1px 2px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 20px rgba(0, 242, 255, 0.5);
|
||||||
|
background: linear-gradient(90deg, #0040cc 0%, #00d0dd 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
background: #2a3b55;
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.6;
|
||||||
|
box-shadow: none;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-msg {
|
||||||
|
color: #ff6b6b;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-top: 15px;
|
||||||
|
text-align: center;
|
||||||
|
background: rgba(255, 107, 107, 0.1);
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid rgba(255, 107, 107, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-msg {
|
||||||
|
color: #00ff88;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
text-align: center;
|
||||||
|
margin: 20px 0 10px;
|
||||||
|
text-shadow: 0 0 10px rgba(0, 255, 136, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
border: 3px solid rgba(255,255,255,0.1);
|
||||||
|
border-radius: 50%;
|
||||||
|
border-top: 3px solid var(--primary-color);
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<img src="/static/image.jpg" alt="云南AI共生大会" class="header-img" onerror="this.style.display='none'">
|
||||||
|
<h1>添加新嘉宾</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<form id="add-user-form" onsubmit="submitAddUser(event)">
|
||||||
|
<div class="input-group">
|
||||||
|
<label>姓名</label>
|
||||||
|
<input type="text" id="name" required placeholder="请输入嘉宾姓名">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label>手机号码</label>
|
||||||
|
<input type="text" id="phone" required placeholder="请输入手机号码">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label>单位名称 / 行业</label>
|
||||||
|
<input type="text" id="company" placeholder="请输入单位名称或行业信息">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" id="submit-btn">确认添加</button>
|
||||||
|
</form>
|
||||||
|
<div id="submit-error" class="error-msg hidden"></div>
|
||||||
|
<div id="submit-success" class="success-msg hidden"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="text-align: center; margin-top: 20px;">
|
||||||
|
<a href="/" style="color: var(--text-muted); text-decoration: none; border-bottom: 1px dashed var(--text-muted);">返回签到首页</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
async function submitAddUser(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const btn = document.getElementById('submit-btn');
|
||||||
|
const errorDiv = document.getElementById('submit-error');
|
||||||
|
const successDiv = document.getElementById('submit-success');
|
||||||
|
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = '<span class="loader"></span> 提交中...';
|
||||||
|
errorDiv.classList.add('hidden');
|
||||||
|
successDiv.classList.add('hidden');
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
name: document.getElementById('name').value,
|
||||||
|
phone: document.getElementById('phone').value,
|
||||||
|
industry_company: document.getElementById('company').value
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/add-user', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
successDiv.textContent = "添加成功!该嘉宾现在可以进行签到了。";
|
||||||
|
successDiv.classList.remove('hidden');
|
||||||
|
document.getElementById('add-user-form').reset();
|
||||||
|
} else {
|
||||||
|
errorDiv.textContent = data.message || "提交失败";
|
||||||
|
errorDiv.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
errorDiv.textContent = "网络错误,请稍后重试";
|
||||||
|
errorDiv.classList.remove('hidden');
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = '确认添加';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
810
templates/index.html
Normal file
810
templates/index.html
Normal file
@@ -0,0 +1,810 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>云南AI共生大会 - 签到</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--primary-color: #00f2ff; /* Cyan from the AI sphere/text */
|
||||||
|
--secondary-color: #0066ff; /* Rich blue */
|
||||||
|
--bg-color: #050814; /* Deep space blue/black */
|
||||||
|
--card-bg: rgba(12, 24, 50, 0.5); /* Glassy blue tint */
|
||||||
|
--text-color: #ffffff;
|
||||||
|
--text-muted: #b0c4de; /* Light blue-grey */
|
||||||
|
--accent-glow: rgba(0, 242, 255, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
/* Deep radial gradient mimicking the poster's atmosphere */
|
||||||
|
background-image:
|
||||||
|
radial-gradient(circle at 50% 0%, #1a3a75 0%, #050814 60%),
|
||||||
|
radial-gradient(circle at 85% 30%, rgba(0, 242, 255, 0.1) 0%, transparent 40%),
|
||||||
|
radial-gradient(circle at 15% 70%, rgba(0, 102, 255, 0.15) 0%, transparent 40%);
|
||||||
|
background-attachment: fixed;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add a subtle grid or tech-line overlay if desired, but keep it clean for now */
|
||||||
|
body::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background:
|
||||||
|
linear-gradient(rgba(255,255,255,0.03) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(255,255,255,0.03) 1px, transparent 1px);
|
||||||
|
background-size: 50px 50px;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
padding: 30px 20px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
border-bottom: 1px solid rgba(0, 242, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-img {
|
||||||
|
width: 100%;
|
||||||
|
max-height: 220px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
box-shadow: 0 0 20px rgba(0, 242, 255, 0.2);
|
||||||
|
border: 1px solid rgba(0, 242, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2.4rem;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
/* Poster style: Bold white/cyan gradient text */
|
||||||
|
background: linear-gradient(180deg, #ffffff 0%, #d0eaff 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
text-shadow: 0 0 30px rgba(0, 242, 255, 0.4);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-info {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: #dbe4ff;
|
||||||
|
line-height: 1.7;
|
||||||
|
margin-top: 20px;
|
||||||
|
text-align: left;
|
||||||
|
background: rgba(0, 20, 60, 0.4);
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 4px solid var(--primary-color);
|
||||||
|
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: var(--card-bg);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 30px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
border: 1px solid rgba(0, 242, 255, 0.2);
|
||||||
|
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.5), inset 0 0 20px rgba(0, 242, 255, 0.05);
|
||||||
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
box-shadow: 0 8px 40px 0 rgba(0, 0, 0, 0.6), inset 0 0 20px rgba(0, 242, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
input, textarea, select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
border: 1px solid rgba(0, 242, 255, 0.3);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: white;
|
||||||
|
font-size: 1rem;
|
||||||
|
outline: none;
|
||||||
|
transition: all 0.3s;
|
||||||
|
box-shadow: inset 0 2px 5px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus, textarea:focus {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 15px rgba(0, 242, 255, 0.3), inset 0 2px 5px rgba(0,0,0,0.3);
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Placeholder styling */
|
||||||
|
::placeholder {
|
||||||
|
color: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 16px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
/* Gradient button */
|
||||||
|
background: linear-gradient(90deg, #0051ff 0%, #00f2ff 100%);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 242, 255, 0.3);
|
||||||
|
text-shadow: 0 1px 2px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 20px rgba(0, 242, 255, 0.5);
|
||||||
|
background: linear-gradient(90deg, #0040cc 0%, #00d0dd 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
background: #2a3b55;
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.6;
|
||||||
|
box-shadow: none;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-msg {
|
||||||
|
color: #ff6b6b;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-top: 15px;
|
||||||
|
text-align: center;
|
||||||
|
background: rgba(255, 107, 107, 0.1);
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid rgba(255, 107, 107, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-msg {
|
||||||
|
color: #00ff88;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
text-align: center;
|
||||||
|
margin: 20px 0 10px;
|
||||||
|
text-shadow: 0 0 10px rgba(0, 255, 136, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.seat-info {
|
||||||
|
font-size: 3rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #fff;
|
||||||
|
margin: 30px 0;
|
||||||
|
text-shadow: 0 0 30px var(--primary-color);
|
||||||
|
padding: 30px;
|
||||||
|
border: 2px solid var(--primary-color);
|
||||||
|
border-radius: 16px;
|
||||||
|
background: rgba(0, 242, 255, 0.05);
|
||||||
|
box-shadow: 0 0 40px rgba(0, 242, 255, 0.15), inset 0 0 20px rgba(0, 242, 255, 0.1);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Shine effect on seat info */
|
||||||
|
.seat-info::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -50%;
|
||||||
|
left: -50%;
|
||||||
|
width: 200%;
|
||||||
|
height: 200%;
|
||||||
|
background: linear-gradient(45deg, transparent, rgba(255,255,255,0.1), transparent);
|
||||||
|
transform: rotate(45deg);
|
||||||
|
animation: shine 3s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shine {
|
||||||
|
0% { transform: translateX(-100%) rotate(45deg); }
|
||||||
|
100% { transform: translateX(100%) rotate(45deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-preview {
|
||||||
|
background: rgba(0, 81, 255, 0.15);
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
border: 1px solid rgba(0, 81, 255, 0.4);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-preview h3 {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-preview p {
|
||||||
|
color: #b0c4de;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading spinner */
|
||||||
|
.loader {
|
||||||
|
border: 3px solid rgba(255,255,255,0.1);
|
||||||
|
border-radius: 50%;
|
||||||
|
border-top: 3px solid var(--primary-color);
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-select-item {
|
||||||
|
padding: 15px;
|
||||||
|
background: rgba(255,255,255,0.05);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-select-item:hover {
|
||||||
|
background: rgba(0, 242, 255, 0.1);
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tablemates styling */
|
||||||
|
.tablemates-section {
|
||||||
|
margin-top: 25px;
|
||||||
|
border-top: 1px solid rgba(255,255,255,0.1);
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tablemate-card {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 12px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
transition: transform 0.2s;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tablemate-card:hover {
|
||||||
|
transform: translateX(5px);
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tablemate-avatar {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, var(--secondary-color), var(--primary-color));
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-right: 15px;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tablemate-info {
|
||||||
|
text-align: left;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tablemate-name {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tablemate-industry {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.click-hint {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--primary-color);
|
||||||
|
opacity: 0.7;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal Styling */
|
||||||
|
.modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1000;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
background-color: rgba(0,0,0,0.8);
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: var(--bg-color);
|
||||||
|
margin: 15% auto;
|
||||||
|
padding: 25px;
|
||||||
|
border: 1px solid var(--primary-color);
|
||||||
|
border-radius: 15px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 500px;
|
||||||
|
box-shadow: 0 0 30px rgba(0, 242, 255, 0.3);
|
||||||
|
position: relative;
|
||||||
|
animation: modalOpen 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modalOpen {
|
||||||
|
from {opacity: 0; transform: translateY(-50px);}
|
||||||
|
to {opacity: 1; transform: translateY(0);}
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
color: #aaa;
|
||||||
|
float: right;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close:hover,
|
||||||
|
.close:focus {
|
||||||
|
color: var(--primary-color);
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-label {
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-value {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<img src="/static/image.jpg" alt="云南AI共生大会" class="header-img" onerror="this.style.display='none'">
|
||||||
|
<div style="text-align: center; margin-bottom: 10px; color: var(--primary-color); font-size: 0.8rem; letter-spacing: 2px; font-weight: 600; opacity: 0.9;">
|
||||||
|
2026 INTELLIGENT LEADERSHIP • AI SYMBIOSIS
|
||||||
|
</div>
|
||||||
|
<h1>云南AI共生大会</h1>
|
||||||
|
<div class="event-info">
|
||||||
|
<p>📅 <strong>时间:</strong>1月10日 下午 2:00</p>
|
||||||
|
<p>📍 <strong>地点:</strong>金鼎科技园18号平台B座一楼报告厅</p>
|
||||||
|
<p>💡 <strong>内容:</strong>邀请重磅大咖分享AI在各行业的企业应用及案例,含深度交流环节。</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Step 1: Search -->
|
||||||
|
<div id="step-search" class="card">
|
||||||
|
<h2 style="margin-bottom: 20px;">嘉宾签到</h2>
|
||||||
|
<div class="input-group">
|
||||||
|
<label>请输入手机号码或姓名</label>
|
||||||
|
<input type="text" id="search-input" placeholder="例如:13800000000 或 张三">
|
||||||
|
</div>
|
||||||
|
<button onclick="searchUser()" id="search-btn">查询</button>
|
||||||
|
<div id="search-error" class="error-msg hidden"></div>
|
||||||
|
<div id="user-list" class="hidden" style="margin-top: 15px;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 2: Fill Info -->
|
||||||
|
<div id="step-form" class="card hidden">
|
||||||
|
<div class="user-preview">
|
||||||
|
<h3 id="display-name"></h3>
|
||||||
|
<p id="display-phone"></p>
|
||||||
|
<p id="display-company" style="font-size: 0.8rem; margin-top: 5px;"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="checkin-form" onsubmit="submitCheckin(event)">
|
||||||
|
<input type="hidden" id="gsdh-id">
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label>姓名</label>
|
||||||
|
<input type="text" id="form-name" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label>手机号码 (不公开)</label>
|
||||||
|
<input type="text" id="form-phone" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label>单位名称</label>
|
||||||
|
<input type="text" id="form-company" placeholder="点此输入">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label>职务 (不公开)</label>
|
||||||
|
<input type="text" id="form-position" placeholder="点此输入">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label>公司主要经营 / 业务</label>
|
||||||
|
<textarea id="form-business" rows="2" placeholder="点此输入"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label>2026年业务愿景</label>
|
||||||
|
<textarea id="form-vision" rows="3" placeholder="点此输入 (为了业务更好对接,最好表述清晰)"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" id="form-location" value="">
|
||||||
|
|
||||||
|
<button type="submit" id="submit-btn">确认签到</button>
|
||||||
|
</form>
|
||||||
|
<div id="submit-error" class="error-msg hidden"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 3: Success -->
|
||||||
|
<div id="step-success" class="card hidden" style="text-align: center; padding: 40px 20px;">
|
||||||
|
<div style="font-size: 4rem; margin-bottom: 20px;">🎉</div>
|
||||||
|
<h2 class="success-msg">签到成功!</h2>
|
||||||
|
<p style="color: var(--text-muted); margin-bottom: 20px;">您的座位已分配</p>
|
||||||
|
|
||||||
|
<div id="seat-display" class="seat-info">
|
||||||
|
<!-- Seat number will be inserted here -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="tablemates-container" class="tablemates-section hidden">
|
||||||
|
<p style="color: var(--primary-color); margin-bottom: 15px; font-size: 0.9rem; text-transform: uppercase; letter-spacing: 1px;">同桌嘉宾推荐 <span style="font-size: 0.7em; opacity: 0.7; text-transform: none;">(点击卡片查看详情)</span></p>
|
||||||
|
<div id="tablemates-list">
|
||||||
|
<!-- Tablemates inserted here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="margin-top: 20px;">请入座等待会议开始</p>
|
||||||
|
<button onclick="resetFlow()" style="margin-top: 30px; background: transparent; border: 1px solid rgba(255,255,255,0.2);">返回首页</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 4: Already Signed -->
|
||||||
|
<div id="step-already-signed" class="card hidden" style="text-align: center; padding: 40px 20px;">
|
||||||
|
<div style="font-size: 4rem; margin-bottom: 20px;">✅</div>
|
||||||
|
<h2 class="success-msg" style="color: #00f2ff;">您已完成签到</h2>
|
||||||
|
<div class="user-preview" style="margin: 20px 0; background: rgba(255,255,255,0.05); border: none;">
|
||||||
|
<h3 id="signed-name" style="margin-bottom: 5px;"></h3>
|
||||||
|
<p id="signed-phone" style="opacity: 0.7;"></p>
|
||||||
|
</div>
|
||||||
|
<p style="color: var(--text-muted); margin-bottom: 20px;">您的座位是</p>
|
||||||
|
|
||||||
|
<div id="signed-seat-display" class="seat-info">
|
||||||
|
<!-- Seat number will be inserted here -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="signed-tablemates-container" class="tablemates-section hidden">
|
||||||
|
<p style="color: var(--primary-color); margin-bottom: 15px; font-size: 0.9rem; text-transform: uppercase; letter-spacing: 1px;">同桌嘉宾推荐 <span style="font-size: 0.7em; opacity: 0.7; text-transform: none;">(点击卡片查看详情)</span></p>
|
||||||
|
<div id="signed-tablemates-list">
|
||||||
|
<!-- Tablemates inserted here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="margin-top: 20px;">请入座等待会议开始</p>
|
||||||
|
<button onclick="resetFlow()" style="margin-top: 30px; background: transparent; border: 1px solid rgba(255,255,255,0.2);">返回首页</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal for details -->
|
||||||
|
<div id="detail-modal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span class="close" onclick="closeModal()">×</span>
|
||||||
|
<div style="text-align: center; margin-bottom: 20px;">
|
||||||
|
<div id="modal-avatar" style="width: 60px; height: 60px; border-radius: 50%; background: linear-gradient(135deg, var(--secondary-color), var(--primary-color)); display: inline-flex; align-items: center; justify-content: center; font-weight: bold; font-size: 1.5rem; margin-bottom: 10px;"></div>
|
||||||
|
<h2 id="modal-name" style="color: #fff; margin-bottom: 5px;"></h2>
|
||||||
|
<p id="modal-industry" style="color: var(--primary-color); font-size: 0.9rem;"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-item">
|
||||||
|
<div class="detail-label">单位名称</div>
|
||||||
|
<div id="modal-company" class="detail-value"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-item">
|
||||||
|
<div class="detail-label">职位</div>
|
||||||
|
<div id="modal-position" class="detail-value"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-item">
|
||||||
|
<div class="detail-label">公司主要经营 / 业务</div>
|
||||||
|
<div id="modal-business" class="detail-value"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-item" style="border-bottom: none;">
|
||||||
|
<div class="detail-label">2026年业务愿景</div>
|
||||||
|
<div id="modal-vision" class="detail-value"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Store selected user data
|
||||||
|
let currentUser = null;
|
||||||
|
|
||||||
|
async function searchUser() {
|
||||||
|
const query = document.getElementById('search-input').value.trim();
|
||||||
|
const btn = document.getElementById('search-btn');
|
||||||
|
const errorDiv = document.getElementById('search-error');
|
||||||
|
const listDiv = document.getElementById('user-list');
|
||||||
|
|
||||||
|
if (!query) {
|
||||||
|
errorDiv.textContent = "请输入查询内容";
|
||||||
|
errorDiv.classList.remove('hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset UI
|
||||||
|
errorDiv.classList.add('hidden');
|
||||||
|
listDiv.classList.add('hidden');
|
||||||
|
listDiv.innerHTML = '';
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = '<span class="loader"></span> 查询中...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/search?query=${encodeURIComponent(query)}`);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
if (data.multiple) {
|
||||||
|
// Handle multiple results
|
||||||
|
listDiv.classList.remove('hidden');
|
||||||
|
listDiv.innerHTML = '<p style="margin-bottom:10px; color:var(--text-muted)">查询到多位嘉宾,请选择:</p>';
|
||||||
|
data.users.forEach(user => {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'user-select-item';
|
||||||
|
item.innerHTML = `<strong>${user.name}</strong> <span style="font-size:0.8em; opacity:0.7">(${user.phone})</span>`;
|
||||||
|
item.onclick = () => selectUser(user);
|
||||||
|
listDiv.appendChild(item);
|
||||||
|
});
|
||||||
|
} else if (data.already_signed) {
|
||||||
|
// Show already signed screen
|
||||||
|
document.getElementById('step-search').classList.add('hidden');
|
||||||
|
document.getElementById('step-already-signed').classList.remove('hidden');
|
||||||
|
|
||||||
|
document.getElementById('signed-name').textContent = data.user.name;
|
||||||
|
document.getElementById('signed-phone').textContent = data.user.phone;
|
||||||
|
document.getElementById('signed-seat-display').textContent = data.seat || "自由席";
|
||||||
|
|
||||||
|
// Show tablemates if available
|
||||||
|
if (data.tablemates && data.tablemates.length > 0) {
|
||||||
|
renderTablemates(data.tablemates, 'signed-tablemates-list', 'signed-tablemates-container');
|
||||||
|
} else {
|
||||||
|
document.getElementById('signed-tablemates-container').classList.add('hidden');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Single user found
|
||||||
|
selectUser(data.user);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
errorDiv.textContent = data.message || "查询失败";
|
||||||
|
errorDiv.classList.remove('hidden');
|
||||||
|
errorDiv.style.color = '#ff4d4d';
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
errorDiv.textContent = "网络错误,请稍后重试";
|
||||||
|
errorDiv.classList.remove('hidden');
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = '查询';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectUser(user) {
|
||||||
|
currentUser = user;
|
||||||
|
|
||||||
|
// Populate form
|
||||||
|
document.getElementById('gsdh-id').value = user.new_id;
|
||||||
|
document.getElementById('form-name').value = user.name;
|
||||||
|
document.getElementById('form-phone').value = user.phone;
|
||||||
|
document.getElementById('form-company').value = user.industry_company || ''; // Pre-fill if we have something roughly mapping
|
||||||
|
|
||||||
|
// Display preview
|
||||||
|
document.getElementById('display-name').textContent = user.name;
|
||||||
|
document.getElementById('display-phone').textContent = user.phone;
|
||||||
|
document.getElementById('display-company').textContent = user.industry_company || '暂无单位信息';
|
||||||
|
|
||||||
|
// Switch steps
|
||||||
|
document.getElementById('step-search').classList.add('hidden');
|
||||||
|
document.getElementById('step-form').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitCheckin(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const btn = document.getElementById('submit-btn');
|
||||||
|
const errorDiv = document.getElementById('submit-error');
|
||||||
|
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = '<span class="loader"></span> 提交中...';
|
||||||
|
errorDiv.classList.add('hidden');
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
gsdh_id: document.getElementById('gsdh-id').value,
|
||||||
|
name: document.getElementById('form-name').value,
|
||||||
|
phone: document.getElementById('form-phone').value,
|
||||||
|
company_name: document.getElementById('form-company').value,
|
||||||
|
position: document.getElementById('form-position').value,
|
||||||
|
business_scope: document.getElementById('form-business').value,
|
||||||
|
vision_2026: document.getElementById('form-vision').value,
|
||||||
|
location: document.getElementById('form-location').value
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/checkin', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
// Show assigned seat
|
||||||
|
document.getElementById('seat-display').textContent = data.seat || "自由席";
|
||||||
|
|
||||||
|
// Show tablemates if available
|
||||||
|
if (data.tablemates && data.tablemates.length > 0) {
|
||||||
|
renderTablemates(data.tablemates, 'tablemates-list', 'tablemates-container');
|
||||||
|
} else {
|
||||||
|
document.getElementById('tablemates-container').classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('step-form').classList.add('hidden');
|
||||||
|
document.getElementById('step-success').classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
errorDiv.textContent = data.message || "提交失败";
|
||||||
|
errorDiv.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
errorDiv.textContent = "网络错误,请稍后重试";
|
||||||
|
errorDiv.classList.remove('hidden');
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = '确认签到';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTablemates(tablemates, listId, containerId) {
|
||||||
|
const list = document.getElementById(listId);
|
||||||
|
list.innerHTML = '';
|
||||||
|
|
||||||
|
tablemates.forEach(mate => {
|
||||||
|
const initial = mate.name ? mate.name.charAt(0) : '?';
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'tablemate-card';
|
||||||
|
div.innerHTML = `
|
||||||
|
<div class="tablemate-avatar">${initial}</div>
|
||||||
|
<div class="tablemate-info">
|
||||||
|
<div class="tablemate-name">${mate.name} <span class="click-hint">点击详情</span></div>
|
||||||
|
<div class="tablemate-industry">${mate.industry}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
// Add click event listener for details
|
||||||
|
div.onclick = () => showModal(mate);
|
||||||
|
list.appendChild(div);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById(containerId).classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showModal(mate) {
|
||||||
|
const modal = document.getElementById('detail-modal');
|
||||||
|
const initial = mate.name ? mate.name.charAt(0) : '?';
|
||||||
|
|
||||||
|
document.getElementById('modal-avatar').textContent = initial;
|
||||||
|
document.getElementById('modal-name').textContent = mate.name;
|
||||||
|
document.getElementById('modal-industry').textContent = mate.industry;
|
||||||
|
document.getElementById('modal-company').textContent = mate.company_name || '未填写';
|
||||||
|
document.getElementById('modal-position').textContent = mate.position || '未填写';
|
||||||
|
document.getElementById('modal-business').textContent = mate.business_scope || '未填写';
|
||||||
|
document.getElementById('modal-vision').textContent = mate.vision_2026 || '未填写';
|
||||||
|
|
||||||
|
modal.style.display = "block";
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
document.getElementById('detail-modal').style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close modal when clicking outside
|
||||||
|
window.onclick = function(event) {
|
||||||
|
const modal = document.getElementById('detail-modal');
|
||||||
|
if (event.target == modal) {
|
||||||
|
modal.style.display = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetFlow() {
|
||||||
|
document.getElementById('step-success').classList.add('hidden');
|
||||||
|
document.getElementById('step-already-signed').classList.add('hidden');
|
||||||
|
document.getElementById('step-search').classList.remove('hidden');
|
||||||
|
document.getElementById('search-input').value = '';
|
||||||
|
document.getElementById('user-list').innerHTML = '';
|
||||||
|
document.getElementById('checkin-form').reset();
|
||||||
|
currentUser = null;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user