Browse Source

Add HTML templates and static file directories

wavesign
NinjaKelly 2 months ago
parent
commit
44d001eb1b
59 changed files with 9825 additions and 0 deletions
  1. +523
    -0
      static/css/Community.css
  2. +835
    -0
      static/css/LifeServing.css
  3. +538
    -0
      static/css/SLClassroom.css
  4. +438
    -0
      static/css/Schedule.css
  5. +59
    -0
      static/css/login_button.css
  6. +931
    -0
      static/css/myPage.css
  7. +216
    -0
      static/data/你好.json
  8. +323
    -0
      static/data/早上好.json
  9. +430
    -0
      static/data/谢谢.json
  10. BIN
      static/images/1.png
  11. BIN
      static/images/2.jpg
  12. BIN
      static/images/3.jpg
  13. BIN
      static/images/OIP-C.jpg
  14. BIN
      static/images/community_video.mp4
  15. BIN
      static/images/default_avatar.png
  16. BIN
      static/images/item1.png
  17. BIN
      static/images/item2.png
  18. BIN
      static/images/item3.png
  19. BIN
      static/images/mapgoing.jpg
  20. BIN
      static/images/profile/baking.jpg
  21. BIN
      static/images/profile/family.jpg
  22. BIN
      static/images/profile/game.jpg
  23. BIN
      static/images/profile/travel.jpg
  24. BIN
      static/images/profile/tree.jpeg
  25. BIN
      static/images/profile2.png
  26. BIN
      static/images/profile3.png
  27. BIN
      static/images/proflie1.png
  28. BIN
      static/images/readme/手势识别1.png
  29. BIN
      static/images/readme/手势识别2.png
  30. BIN
      static/images/recommendGoods.jpg
  31. BIN
      static/images/recruit.jpg
  32. BIN
      static/images/sign_good.png
  33. BIN
      static/images/sign_thank_you.png
  34. BIN
      static/images/sign_you.png
  35. BIN
      static/images/登录1.jpg
  36. BIN
      static/images/聊天2.jpg
  37. BIN
      static/images/聊天头像2.jpg
  38. +71
    -0
      static/js/SLClassroom.js
  39. +2
    -0
      static/js/mediapipe/vision_bundle.mjs
  40. +22
    -0
      static/js/mediapipe/wasm/vision_wasm_internal.js
  41. BIN
      static/js/mediapipe/wasm/vision_wasm_internal.wasm
  42. BIN
      static/js/mediapipe/wasm/vision_wasm_nosimd_internal.wasm
  43. +625
    -0
      static/js/myPage.js
  44. +387
    -0
      static/js/upload_tasks_evaluator.js
  45. BIN
      static/models/hand_landmarker.task
  46. BIN
      static/models/vision_wasm_internal.wasm
  47. BIN
      static/models/vision_wasm_nosimd_internal.wasm
  48. BIN
      static/videos/你好.mp4
  49. BIN
      static/videos/早上好.mp4
  50. BIN
      static/videos/谢谢.mp4
  51. +474
    -0
      templates/Community.html
  52. +692
    -0
      templates/Home.html
  53. +468
    -0
      templates/LifeServing.html
  54. +270
    -0
      templates/Login.html
  55. +238
    -0
      templates/Register.html
  56. +389
    -0
      templates/SLClassroom.html
  57. +983
    -0
      templates/Schedule.html
  58. +469
    -0
      templates/chat.html
  59. +442
    -0
      templates/myPage.html

+ 523
- 0
static/css/Community.css View File

@ -0,0 +1,523 @@
:root {
--primary-purple: #6a1b9a;
--secondary-purple: #9c27b0;
--light-purple: #d1c4e9;
--dark-purple: #4a148c;
--background: #f3e5f5;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Segoe UI', system-ui, sans-serif;
}
body {
background: var(--background);
min-height: 100vh;
padding: 20px;
}
/* 新增内容容器边距 */
.main-container {
padding: 0 2rem;
/* 内容区二级边距 */
margin: 0 auto;
}
.card {
background-color: var(--card-bg);
border: 1px solid var(--border-color);
transition: transform .3s, box-shadow .3s, border-color .3s;
margin: 1rem 0;
/* 新增卡片外边距 */
padding: 1.5rem;
/* 新增卡片内边距[2](@ref) */
}
.card:hover {
border-color: var(--primary);
transform: translateY(-5px);
box-shadow: 0 10px 20px #0000001a
}
.btn {
transition: transform .2s, background-color .2s
}
.btn:hover {
transform: scale(1.05)
}
.fade-in {
animation: .8s ease-in fadeIn
}
@keyframes fadeIn {
0% {
opacity: 0;
transform: translateY(20px)
}
to {
opacity: 1;
transform: translateY(0)
}
}
/* 导航栏样式 */
.nav-link {
position: relative
}
.nav-link:after {
content: "";
background-color: var(--primary);
width: 0;
height: 2px;
transition: width .3s;
position: absolute;
bottom: -2px;
left: 0
}
.nav-link:hover:after,.nav-link.active:after {
width: 100%
}
.navbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 30px;
background: linear-gradient(135deg, #E6E6FA, #D8BFD8); /* 浅紫色渐变背景 */
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
border-bottom-left-radius: 30px;
border-bottom-right-radius: 30px;
margin-bottom: -10px;
}
/* Logo 样式 */
.navbar .logo {
font-size: 28px;
font-weight: bold;
color: #4B0082;
}
/* 标题 样式 */
.title-highlight {
font-size: 30px;
font-weight: bold;
color: #fff;
background: #7B1FA2;
padding: 10px 20px;
border-radius: 20px;
box-shadow: 3px 3px 12px rgba(0, 0, 0, 0.2);
position: relative;
display: inline-block;
transition: transform 0.3s ease-in-out;
}
.title-highlight::after {
content: '';
position: absolute;
left: 50%;
bottom: -6px;
width: 60%;
height: 5px;
background: #BA68C8;
border-radius: 10px;
transform: translateX(-50%);
transition: width 0.3s ease-in-out;
}
.title-highlight:hover {
transform: translateY(-5px) scale(1.05);
}
.title-highlight:hover::after {
width: 80%;
}
.nav-menu {
display: flex;
flex: 1;
justify-content: space-between;
max-width: 600px;
}
.nav-item {
color: white;
padding: 8px 20px;
border-radius: 20px;
transition: all 0.3s;
text-align: center;
font-size: 16px;
}
.nav-item:hover {
background: rgba(255,255,255,0.15);
}
.search-box {
margin-left: auto;
display: flex;
align-items: center;
}
.search-input {
padding: 8px 15px;
border-radius: 20px;
border: 2px solid var(--light-purple);
background: rgba(255,255,255,0.9);
width: 240px;
}
/* 主内容区 */
.container {
margin: 20px auto;
margin-left: 50px;
width: 90%;
display: grid;
grid-template-columns: 2fr 1fr;
gap: 30px;
}
/* 动态编辑器 */
.post-editor {
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 2px 8px rgba(106,27,154,0.1);
}
.editor-input {
width: 100%;
min-height: 100px;
border: 2px solid var(--light-purple);
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
font-size: 16px;
}
.post-actions {
display: flex;
justify-content: space-between;
align-items: center;
}
.action-buttons button {
background: var(--light-purple);
color: var(--dark-purple);
padding: 8px 15px;
border-radius: 20px;
margin-right: 10px;
transition: all 0.3s;
}
.action-buttons button:hover {
background: var(--secondary-purple);
color: white;
}
.post-button {
background: var(--primary-purple);
color: white;
padding: 10px 30px;
border-radius: 25px;
font-weight: 500;
transition: transform 0.2s;
}
.post-button:hover {
transform: translateY(-2px);
}
/* 动态卡片 */
.post-card {
background: white;
border-radius: 12px;
padding: 20px;
margin-top: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(106,27,154,0.1);
}
.user-avatar {
width: 50px;
height: 50px;
border-radius: 50%;
border: 2px solid var(--light-purple);
}
.post-header {
display: flex;
gap: 15px;
align-items: center;
margin-bottom: 15px;
}
.post-content {
color: var(--text-dark);
line-height: 1.6;
margin-bottom: 15px;
}
.post-media{
width: 80%;
height: auto;
border-radius: 8px;
margin-top: 10px;
margin-bottom: 15px;
}
.tag {
display: inline-block;
background: var(--light-purple);
color: var(--dark-purple);
padding: 4px 12px;
border-radius: 15px;
margin-right: 8px;
margin-bottom: 15px;
font-size: 14px;
}
/* 侧边栏 */
.sidebar {
position: sticky;
top: 80px;
height: fit-content;
}
.trending-topics {
background: white;
padding: 20px;
border-radius: 12px;
width: 80%;
box-shadow: 0 2px 8px rgba(106,27,154,0.1);
}
.topic-item {
padding: 10px;
margin: 8px 0;
background: var(--light-purple);
border-radius: 8px;
color: var(--dark-purple);
transition: all 0.3s;
}
.topic-item:hover {
background: var(--secondary-purple);
color: white;
}
/* 响应式设计 */
@media (max-width: 1200px) {
.container {
width: 90%;
grid-template-columns: 1fr;
}
.nav-menu {
display: none;
}
.search-box {
margin-left: 50px;
}
}
/* 活动推荐模块 */
.container-0 {
max-width: 1400px;
margin: 0 auto;
padding: 10px;
margin-top: 5px;
}
.activity-section {
padding: 60px 0;
}
.section-title {
text-align: center;
font-size: 2.4rem;
color: var(--dark-purple);
margin-bottom: 50px;
position: relative;
}
.section-title::after {
content: '';
position: absolute;
bottom: -15px;
left: 50%;
transform: translateX(-50%);
width: 120px;
height: 3px;
background: var(--primary-purple);
}
.news-list {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 30px;
padding: 0 20px;
}
.news-item {
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 6px 15px rgba(106,27,154,0.1);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.news-item:hover {
transform: translateY(-5px);
box-shadow: 0 8px 20px rgba(106,27,154,0.15);
}
.news-media {
position: relative;
height: 220px;
overflow: hidden;
}
.news-image {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.4s ease;
}
.news-item:hover .news-image {
transform: scale(1.05);
}
.news-meta {
position: absolute;
bottom: 0;
left: 0;
right: 0;
display: flex;
justify-content: space-between;
padding: 12px 20px;
background: linear-gradient(0deg, rgba(106,27,154,0.9) 0%, transparent 100%);
color: white;
}
.news-date {
font-size: 0.9rem;
opacity: 0.9;
}
.news-category {
background: var(--light-purple);
color: var(--dark-purple);
padding: 6px 15px;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 500;
}
.news-content {
padding: 20px;
}
.news-title {
color: var(--dark-purple);
font-size: 1.3rem;
margin-bottom: 12px;
line-height: 1.4;
min-height: 60px;
}
.news-desc {
color: #666;
font-size: 0.95rem;
line-height: 1.6;
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
min-height: 72px;
}
/* 更多活动提示 */
.news-item:last-child {
grid-column: 1 / -1;
display: flex;
align-items: center;
justify-content: center;
background: var(--light-purple);
min-height: 150px;
border: 2px dashed var(--primary-purple);
}
.news-item:last-child h4 {
color: var(--dark-purple);
font-size: 1.6rem;
margin: 0;
padding: 30px;
text-align: center;
}
/* 响应式设计 */
@media (max-width: 1440px) {
.news-list {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 1200px) {
.container {
padding: 20px;
}
.news-list {
grid-template-columns: repeat(2, 1fr);
}
.section-title {
font-size: 2rem;
}
}
@media (max-width: 768px) {
.news-list {
grid-template-columns: 1fr;
}
.news-item:last-child {
min-height: 100px;
}
.news-item:last-child h4 {
font-size: 1.2rem;
padding: 20px;
}
}
@media (max-width: 480px) {
.section-title {
font-size: 1.8rem;
}
.news-media {
height: 180px;
}
.news-title {
font-size: 1.1rem;
min-height: auto;
}
.news-desc {
min-height: 54px;
}
}

+ 835
- 0
static/css/LifeServing.css View File

@ -0,0 +1,835 @@
:root {
--primary-purple: #6a1b9a;
--secondary-purple: #9c27b0;
--light-purple: #d1c4e9;
--dark-purple: #4a148c;
--background: #f3e5f5;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Segoe UI', system-ui, sans-serif;
}
body {
background: var(--background);
min-height: 100vh;
padding: 20px;
}
/* 新增内容容器边距 */
.main-container {
padding: 0 2rem;
/* 内容区二级边距 */
margin: 0 auto;
}
.card {
background-color: var(--card-bg);
border: 1px solid var(--border-color);
transition: transform .3s, box-shadow .3s, border-color .3s;
margin: 1rem 0;
/* 新增卡片外边距 */
padding: 1.5rem;
/* 新增卡片内边距[2](@ref) */
}
.card:hover {
border-color: var(--primary);
transform: translateY(-5px);
box-shadow: 0 10px 20px #0000001a
}
.btn {
transition: transform .2s, background-color .2s
}
.btn:hover {
transform: scale(1.05)
}
.fade-in {
animation: .8s ease-in fadeIn
}
@keyframes fadeIn {
0% {
opacity: 0;
transform: translateY(20px)
}
to {
opacity: 1;
transform: translateY(0)
}
}
/* 导航栏样式 */
.nav-link {
position: relative
}
.nav-link:after {
content: "";
background-color: var(--primary);
width: 0;
height: 2px;
transition: width .3s;
position: absolute;
bottom: -2px;
left: 0
}
.nav-link:hover:after,.nav-link.active:after {
width: 100%
}
.navbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 30px;
background: linear-gradient(135deg, #E6E6FA, #D8BFD8); /* 浅紫色渐变背景 */
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
border-bottom-left-radius: 30px;
border-bottom-right-radius: 30px;
}
/* Logo 样式 */
.navbar .logo {
font-size: 28px;
font-weight: bold;
color: #4B0082;
}
/* 标题 样式 */
.title-highlight {
font-size: 30px;
font-weight: bold;
color: #fff;
background: #7B1FA2;
padding: 10px 20px;
border-radius: 20px;
box-shadow: 3px 3px 12px rgba(0, 0, 0, 0.2);
position: relative;
display: inline-block;
transition: transform 0.3s ease-in-out;
}
.title-highlight::after {
content: '';
position: absolute;
left: 50%;
bottom: -6px;
width: 60%;
height: 5px;
background: #BA68C8;
border-radius: 10px;
transform: translateX(-50%);
transition: width 0.3s ease-in-out;
}
.title-highlight:hover {
transform: translateY(-5px) scale(1.05);
}
.title-highlight:hover::after {
width: 80%;
}
/* 导航栏样式 */
.nav-container {
background: white;
padding: 1rem 2rem;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
margin: 2rem auto;
max-width: 1300px;
font-size:larger;
}
.nav-menu {
display: flex;
gap: 10rem;
list-style: none;
}
.nav-item {
position: relative;
padding: 0.8rem 1.5rem;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
color: var(--dark-purple);
font-size: large;
font-weight: 700;
}
.nav-item:hover {
background: var(--light-purple);
transform: translateY(-2px);
}
.nav-item:hover::after {
content: '';
position: absolute;
bottom: -5px;
left: 50%;
transform: translateX(-50%);
width: 80%;
height: 3px;
background: var(--primary-purple);
}
/* 内容容器 */
.content-container {
display: none;
max-width: 1300px;
margin: 2rem auto;
background: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
padding: 2rem;
}
/* 默认显示第一个模块 */
#travel-service {
display: block;
}
/* 保持原有卡片样式 */
.service-card {
background: #f8f5fc;
padding: 1.5rem;
border-radius: 8px;
margin-bottom: 1.5rem;
border-left: 4px solid var(--primary-purple);
}
/* 响应式设计 */
@media (max-width: 768px) {
.nav-menu {
flex-direction: column;
gap: 1rem;
}
.nav-item {
text-align: center;
}
}
.traffic-wrapper {
display: grid;
grid-template-columns: 1fr 300px;
gap: 20px;
margin-bottom: 2rem;
}
.map-container {
width: 100%;
height: 400px;
border-radius: 12px;
overflow: hidden;
position: relative;
}
.traffic-legend {
position: absolute;
bottom: 20px;
left: 20px;
background: rgba(255,255,255,0.9);
padding: 12px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(106,27,154,0.1);
}
.traffic-dot {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 8px;
}
.traffic-smooth { background: #4CAF50; }
.traffic-slow { background: #FFC107; }
.traffic-congested { background: #F44336; }
.traffic-sidebar {
background: #f8f5fc;
padding: 20px;
border-radius: 12px;
}
.alert-list li {
padding: 10px;
margin: 8px 0;
background: white;
border-left: 3px solid var(--primary-purple);
border-radius: 6px;
}
.accessibility-recommend {
display: grid;
grid-template-columns: repeat(3,1fr);
gap: 20px;
}
.recommend-card {
text-align: center;
padding: 20px;
background: white;
border-radius: 12px;
transition: transform 0.3s;
}
.recommend-card:hover {
transform: translateY(-5px);
}
.recommend-card .icon {
font-size: 2.5rem;
margin-bottom: 1rem;
}
.purple-btn {
background: var(--primary-purple);
color: white;
border: none;
margin-top: 20px;
padding: 8px 20px;
border-radius: 20px;
cursor: pointer;
transition: background 0.3s;
}
.purple-btn:hover {
background: var(--dark-purple);
}
/* 好物推荐样式 */
.category-nav {
display: flex;
gap: 15px;
margin-bottom: 2rem;
padding: 1rem;
background: #f8f5fc;
border-radius: 12px;
}
.category-btn {
padding: 8px 20px;
border: 2px solid var(--light-purple);
border-radius: 20px;
background: transparent;
cursor: pointer;
transition: all 0.3s;
}
.category-btn.active {
background: var(--primary-purple);
color: white;
border-color: var(--primary-purple);
}
.goods-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 25px;
}
.goods-card {
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(106,27,154,0.1);
transition: transform 0.3s;
}
.goods-card:hover {
transform: translateY(-5px);
}
.goods-img {
height: 220px;
background: #d1c4e9;
position: relative;
background-size: cover;
background-position: center;
}
.favorite-btn {
position: absolute;
top: 10px;
right: 10px;
background: rgba(255,255,255,0.9);
padding: 6px 12px;
border-radius: 20px;
border: none;
cursor: pointer;
}
.goods-info {
padding: 1.5rem;
}
.price-tag {
margin: 1rem 0;
display: flex;
align-items: center;
gap: 10px;
}
.current-price {
color: var(--primary-purple);
font-size: 1.4rem;
font-weight: bold;
}
.original-price {
color: #999;
text-decoration: line-through;
}
.buy-btn {
width: 100%;
padding: 12px;
background: var(--primary-purple);
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
transition: background 0.3s;
}
.buy-btn:hover {
background: var(--dark-purple);
}
/* 就业信息样式 */
.job-filter {
background: #f8f5fc;
padding: 20px;
border-radius: 12px;
margin-bottom: 2rem;
}
.search-box {
display: flex;
gap: 15px;
margin-bottom: 1rem;
}
.search-input {
flex: 1;
padding: 12px;
border: 2px solid var(--light-purple);
border-radius: 8px;
}
.search-btn {
background: var(--primary-purple);
color: white;
padding: 12px 25px;
border: none;
border-radius: 8px;
cursor: pointer;
}
.filter-tags {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.tag-btn {
padding: 8px 15px;
border: 2px solid var(--light-purple);
border-radius: 20px;
background: white;
cursor: pointer;
}
.tag-btn.active {
background: var(--primary-purple);
color: white;
border-color: var(--primary-purple);
}
.job-list {
display: grid;
gap: 20px;
}
.job-card {
background: white;
padding: 20px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(106,27,154,0.1);
}
.job-card.highlighted {
border-left: 4px solid var(--primary-purple);
}
.job-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.salary {
color: var(--primary-purple);
font-weight: bold;
}
.job-meta {
display: flex;
gap: 15px;
color: #666;
margin-bottom: 1rem;
}
.job-tags {
display: flex;
gap: 10px;
margin-bottom: 1rem;
}
.tag {
padding: 4px 12px;
border-radius: 15px;
font-size: 0.9em;
}
.tag.purple {
background: var(--light-purple);
color: var(--dark-purple);
}
.job-desc {
margin: 1rem 0;
color: #444;
}
.job-desc ul {
padding-left: 20px;
margin-top: 0.5rem;
}
.job-actions {
display: flex;
gap: 15px;
margin-top: 1.5rem;
}
.apply-btn {
background: var(--primary-purple);
color: white;
padding: 10px 25px;
border: none;
border-radius: 20px;
cursor: pointer;
}
.save-btn {
background: none;
border: 2px solid var(--primary-purple);
color: var(--primary-purple);
padding: 10px 25px;
border-radius: 20px;
cursor: pointer;
}
/* Section 容器 */
section {
display: flex;
/* 启用 Flexbox 布局 */
justify-content: space-between;
/* 卡片之间均匀分布 */
gap: 20px;
/* 卡片之间的间距 */
padding: 50px;
background-color: linear-gradient(to right, #830f76, #ce7b9a);
}
/* 卡片样式 */
.card {
background-color: #fff;
border-radius: 50px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
width: calc(33.333% - 30px);
/* 3 列布局,减去间距 */
text-align: center;
padding: 25px;
box-sizing: border-box;
/* 确保 padding 不影响宽度 */
width: 30%;
}
.card:hover {
transform: scale(1.05);
transition: transform 0.3s ease;
}
.card img {
width: 100%;
height: 50%;
border-radius: 10px 10px 0 0;
}
.card h1 {
font-size: 1.5rem;
margin: 10px 0;
}
.card .price {
font-size: 1.25rem;
color: #333;
margin: 10px 0;
}
.card p {
font-size: 1rem;
color: #666;
}
.card button {
background-color: #3498db;
color: #fff;
border: none;
padding: 15px 50px;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.card button:hover {
background-color: #2980b9;
}
/* 响应式设计 */
@media (max-width: 768px) {
section {
flex-wrap: wrap;
/* 小屏幕上卡片换行 */
}
.card {
width: calc(40% - 20px);
/* 2 列布局 */
}
}
@media (max-width: 480px) {
.card {
width: 100%;
/* 1 列布局 */
}
}
.news-container {
max-width: 85%;
margin: 0 auto;
margin-top: 60px;
background: rgb(240, 211, 237);
border-radius: 12px;
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1);
padding: 2rem;
}
/* 区块标题 */
.section-title {
color: #2c3e50;
border-bottom: 3px solid #3498db;
padding-bottom: 0.5rem;
margin-bottom: 2rem;
}
/* 新闻列表 */
.news-list {
display: grid;
gap: 1.5rem;
}
/* 单个新闻项 */
.news-item {
display: grid;
grid-template-columns: 120px 1fr;
gap: 1.5rem;
padding: 1.5rem;
background: #fff;
border-radius: 8px;
transition: all 0.3s ease;
border: 1px solid #eee;
}
.news-media {
position: relative;
overflow: hidden;
}
.news-image {
width: 100%;
height: 80px;
object-fit: cover;
transition: transform 0.3s ease;
}
.news-item:hover {
transform: translateY(-3px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
/* 元信息 */
.news-meta {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.news-date {
color: #7f8c8d;
font-size: 0.9rem;
margin-bottom: 0.5rem;
}
.news-category {
background: #3498db;
color: white;
padding: 0.3rem 0.8rem;
border-radius: 20px;
font-size: 0.8rem;
}
/* 内容区域 */
.news-title {
color: #2c3e50;
margin: 0 0 0.5rem;
font-size: 1.4rem;
}
.news-desc {
color: #7f8c8d;
margin: 0;
font-size: 0.95rem;
}
hr.dashed {
border-top: 3px dashed #bbb;
}
#activity h4 {
color: #b44286;
text-align: center;
margin-bottom: 30px;
font-size: 24px;
position: relative;
}
.news-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
margin-top: 30px;
}
.news-item {
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 3px 6px rgba(0,0,0,0.1);
transition: all 0.3s ease;
}
.news-item:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
}
.news-media {
position: relative;
height: 180px;
overflow: hidden;
}
.news-image {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.5s ease;
}
.news-item:hover .news-image {
transform: scale(1.05);
}
.news-meta {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(0,0,0,0.7);
color: white;
padding: 10px;
display: flex;
justify-content: space-between;
}
.news-date {
font-size: 12px;
}
.news-category {
background: #e74c3c;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
}
.news-content {
padding: 15px;
}
.news-title {
color: #2c3e50;
margin: 0 0 10px;
font-size: 18px;
transition: color 0.3s;
}
.news-item:hover .news-title {
color: #e74c3c;
}

+ 538
- 0
static/css/SLClassroom.css View File

@ -0,0 +1,538 @@
body {
font-family: Arial, sans-serif;
margin: 0;
background-color: #f9f9f9;
}
/* 导航栏样式 */
.navbar {
margin: 0 20px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 30px;
background: linear-gradient(135deg, #E6E6FA, #D8BFD8); /* 浅紫色渐变背景 */
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
border-bottom-left-radius: 30px;
border-bottom-right-radius: 30px;
}
/* Logo 样式 */
.navbar .logo {
font-size: 28px;
font-weight: bold;
color: #4B0082;
}
/* 标题 样式 */
.title-highlight {
font-size: 30px;
font-weight: bold;
color: #fff;
background: #7B1FA2;
padding: 10px 20px;
border-radius: 20px;
box-shadow: 3px 3px 12px rgba(0, 0, 0, 0.2);
position: relative;
display: inline-block;
transition: transform 0.3s ease-in-out;
}
.title-highlight::after {
content: '';
position: absolute;
left: 50%;
bottom: -6px;
width: 60%;
height: 5px;
background: #BA68C8;
border-radius: 10px;
transform: translateX(-50%);
transition: width 0.3s ease-in-out;
}
.title-highlight:hover {
transform: translateY(-5px) scale(1.05);
}
.title-highlight:hover::after {
width: 80%;
}
/* 中间 样式 */
.nav-divider {
flex-grow: 1;
height: 4px;
background: linear-gradient(to right, transparent, #BA68C8, transparent);
border-radius: 10px;
margin: 0 20px;
opacity: 0.8;
transition: opacity 0.3s ease-in-out;
}
.nav-divider:hover {
opacity: 1;
}
/* 图标容器样式 */
.icon-container {
display: flex;
gap: 30px; /* 增加图标间距 */
}
.navbaricon i {
font-size: 25px;
color: #4B0082;
animation: gemAnim 1.5s ease infinite;
}
/* 每个图标的圆形背景 */
.icon-circle {
display: flex;
justify-content: center;
align-items: center;
background-color: #F3E5F5; /* 浅紫色背景 */
border-radius: 50%; /* 圆形背景 */
width: 80px; /* 圆形宽度 */
width: 50px; /* 圆形宽度 */
height: 50px; /* 圆形高度 */
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
/* 图标悬停容器样式 */
.hover-container {
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
.hover-card {
position: absolute;
top: 50px;
left: 50%;
transform: translateX(-50%);
width: 280px; /* 增大卡片宽度 */
background: #F3E5F5;
border: 1px solid #D8BFD8;
border-radius: 12px;
box-shadow: 0 6px 12px rgba(180, 136, 255, 0.3);
padding: 16px;
font-size: 14px;
display: none;
z-index: 100;
text-align: center;
max-width: 90vw; /* 防止超出屏幕 */
max-height: 80vh;
overflow: auto;
}
/* 调整卡片在屏幕边界时的位置 */
.hover-container:hover .hover-card {
display: block;
left: auto;
right: 0;
transform: translateX(0);
}
@media (max-width: 400px) {
.hover-card {
width: 95vw; /* 在小屏幕下占满大部分宽度 */
}
}
/* popout 动画效果 */
@keyframes popout {
0% {
opacity: 0;
transform: translateY(-10px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
/* 图标悬停样式 */
.hover-container i {
transition: transform 0.3s ease, color 0.3s ease;
}
/* 悬停时图标放大 */
.hover-container:hover i {
transform: scale(1.2);
}
/* 进度条样式 */
.progress-bar {
width: 100%;
height: 8px;
background-color: #f0f0f0;
border-radius: 4px;
margin-top: 10px;
}
.progress {
height: 100%;
background-color: #8A2BE2;
border-radius: 4px;
}
/* 日历样式 */
.calendar {
width: 100%;
border-collapse: collapse;
}
.calendar th,
.calendar td {
width: 36px;
/* 适配卡片增大 */
height: 36px;
text-align: center;
border-radius: 8px;
}
.calendar th {
color: #4B0082;
}
.calendar td {
background: #F8F0FF;
cursor: pointer;
}
.calendar td:hover {
background: #E0B0FF;
}
/* 高亮当前日期 */
.current-day {
background: #BA68C8 !important;
color: white;
font-weight: bold;
}
/* 随机选中以前的日期 */
.random-day {
background: #BA68C8 !important;
color: white;
font-weight: bold;
}
/* 我的积分卡片 - 宝石旋转效果 */
.gem-spin i {
animation: gem-spin 2s infinite linear;
}
@keyframes gem-spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* 主内容区域 */
.content {
margin: 40px 40px; /* 上下留白 + 水平居中 */
background: #F3E5F5;
border-radius: 12px;
padding: 30px;
box-shadow: 0 4px 16px rgba(180, 136, 255, 0.2);
display: flex;
flex-direction: row; /* 水平排列子元素 */
gap: 20px; /* 子元素间距 */
border: 1px solid rgba(180, 136, 255, 0.3);
transition: box-shadow 0.3s ease;
}
/* 侧边栏 - 垂直选项卡按钮区域 */
.tab {
display: flex;
flex-direction: column; /* 垂直排列按钮 */
width: 30%; /* 设置为左侧栏的二分之一 */
margin-right: 20px; /* 为按钮区域添加右间距 */
}
/* 为选项卡按钮设置样式 */
.tab button {
background-color: inherit;
color: black;
padding: 16px;
border: 1px solid #ccc;
width: 100%;
border-radius: 20px; /* 改为更圆润的曲线 */
cursor: pointer;
text-align: left;
transition: 0.3s;
font-size: 17px;
margin-bottom: 8px; /* 为按钮之间添加间距 */
background-color: #F1E6FB; /* 浅紫色背景 */
}
/* 悬停时改变按钮的背景颜色 */
.tab button:hover {
background-color: #D1A7F7; /* 更加明显的浅紫色悬停背景 */
border: 1px solid #9C63D1; /* 加强边框颜色 */
}
/* 为活动的选项卡按钮设置样式 */
.tab button.active {
background-color: #9C63D1; /* 选中状态的浅紫色背景 */
color: white; /* 文字变为白色 */
border: 1px solid #9C63D1; /* 选中时边框颜色与背景一致 */
}
/* 选项卡内容区域样式 */
.tabcontent {
display: none; /* 默认隐藏内容 */
padding: 15px;
background-color: #f9f9f9;
border: 1px solid #ccc;
border-top: none;
margin-top: 10px;
border-radius: 8px;
width: 100%; /* 让内容区域自适应填充剩余宽度 */
flex-grow: 1; /* 让内容区域占据剩余空间 */
}
/* 显示活动选项卡内容 */
.tabcontent.active {
display: block;
}
/* 内容区域的背景及边距调整 */
.sidebar .tabcontent {
background-color: #FFF;
margin-top: 10px;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(180, 136, 255, 0.2);
}
/* 高亮显示选中内容 */
.sidebar .tabcontent.active {
background-color: #F3E5F5; /* 给选中内容加个浅紫色背景 */
box-shadow: 0 4px 8px rgba(156, 99, 209, 0.3);
}
/* 地图 */
.map-container {
position: relative;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px 0;
}
/* 进度线 */
.progress-line {
width: 4px;
height: 50px;
background: #777;
margin: 10px 0;
}
/* 已解锁进度线 */
.unlocked-line {
background: linear-gradient(to bottom, #BA68C8, transparent);
}
/* 关卡点 */
.map-point {
width: 120px;
height: 120px;
background: rgba(255, 255, 255, 0.9);
border-radius: 50%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: transform 0.3s, box-shadow 0.3s;
}
/* 已解锁关卡 */
.unlocked {
border: 3px solid #BA68C8;
color: #4B0082;
animation: glow 1.5s infinite alternate;
}
/* 未解锁关卡 */
.locked {
border: 3px dashed #777;
color: #777;
filter: grayscale(50%);
}
/* 动态光晕 */
@keyframes glow {
from {
box-shadow: 0 0 10px #BA68C8;
}
to {
box-shadow: 0 0 20px #BA68C8;
}
}
/* 悬停放大效果 */
.map-point:hover {
transform: scale(1.1);
}
/* 关卡文本 */
.lesson-name {
margin-top: 5px;
font-size: 14px;
}
/* 视频教程 */
.lesson-container {
display: flex;
flex-direction: column;
gap: 20px;
}
.lesson-card {
background: #FFF;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
padding: 15px;
transition: transform 0.3s ease-in-out;
}
.lesson-card:hover {
transform: scale(1.05);
}
.lesson-header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 18px;
font-weight: bold;
margin-bottom: 10px;
}
.completed .completed-icon {
color: #4CAF50;
}
.in-progress .progress-icon {
color: #FFC107;
}
.locked .lock-icon {
color: #9E9E9E;
}
.lesson-progress {
width: 100%;
height: 8px;
background: #E0E0E0;
border-radius: 4px;
margin-top: 10px;
}
.progress-bar {
height: 100%;
background: #BA68C8;
border-radius: 4px;
}
.locked-message {
text-align: center;
font-size: 14px;
color: #777;
}
/* 互动 */
.practice-container {
display: flex;
gap: 20px;
}
.practice-card {
width: 180px;
height: 220px;
background: #FFF;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
cursor: pointer;
transform-style: preserve-3d;
transition: transform 0.6s;
}
.practice-card img {
width: 80px;
height: 80px;
}
.practice-card .card-front,
.practice-card .card-back {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.card-back {
transform: rotateY(180deg);
font-size: 18px;
font-weight: bold;
color: #4B0082;
}
.practice-card.flipped {
transform: rotateY(180deg);
}

+ 438
- 0
static/css/Schedule.css View File

@ -0,0 +1,438 @@
:root {
--primary-purple: #6a1b9a;
--secondary-purple: #9c27b0;
--light-purple: #d1c4e9;
--dark-purple: #4a148c;
--background: #f3e5f5;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Segoe UI', system-ui, sans-serif;
}
body {
background: var(--background);
color: var(--text-color);
min-height: 100vh;
padding: 20px;
}
@media (max-width: 768px) {
body {
padding: 1.5rem;
/* 移动端统一边距 */
}
}
/* 新增内容容器边距 */
.main-container {
padding: 0 2rem;
/* 内容区二级边距 */
margin: 0 auto;
}
.card {
background-color: var(--card-bg);
border: 1px solid var(--border-color);
transition: transform .3s, box-shadow .3s, border-color .3s;
margin: 1rem 0;
/* 新增卡片外边距 */
padding: 1.5rem;
/* 新增卡片内边距[2](@ref) */
}
.card:hover {
border-color: var(--primary);
transform: translateY(-5px);
box-shadow: 0 10px 20px #0000001a
}
.btn {
transition: transform .2s, background-color .2s
}
.btn:hover {
transform: scale(1.05)
}
.fade-in {
animation: .8s ease-in fadeIn
}
@keyframes fadeIn {
0% {
opacity: 0;
transform: translateY(20px)
}
to {
opacity: 1;
transform: translateY(0)
}
}
/* 导航栏样式 */
.nav-link {
position: relative
}
.nav-link:after {
content: "";
background-color: var(--primary);
width: 0;
height: 2px;
transition: width .3s;
position: absolute;
bottom: -2px;
left: 0
}
.nav-link:hover:after,
.nav-link.active:after {
width: 100%
}
.navbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 30px;
background: linear-gradient(135deg, #E6E6FA, #D8BFD8);
/* 浅紫色渐变背景 */
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
border-bottom-left-radius: 30px;
border-bottom-right-radius: 30px;
}
/* Logo 样式 */
.navbar .logo {
font-size: 28px;
font-weight: bold;
color: #4B0082;
}
/* 标题 样式 */
.title-highlight {
font-size: 30px;
font-weight: bold;
color: #fff;
background: #7B1FA2;
padding: 10px 20px;
border-radius: 20px;
box-shadow: 3px 3px 12px rgba(0, 0, 0, 0.2);
position: relative;
display: inline-block;
transition: transform 0.3s ease-in-out;
}
.title-highlight::after {
content: '';
position: absolute;
left: 50%;
bottom: -6px;
width: 60%;
height: 5px;
background: #BA68C8;
border-radius: 10px;
transform: translateX(-50%);
transition: width 0.3s ease-in-out;
}
.title-highlight:hover {
transform: translateY(-5px) scale(1.05);
}
.title-highlight:hover::after {
width: 80%;
}
/* 导航栏样式 */
.nav-container {
background: white;
padding: 1rem 2rem;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
margin: 2rem auto;
max-width: 1300px;
font-size: larger;
}
.nav-menu {
display: flex;
gap: 10rem;
list-style: none;
}
.nav-item {
position: relative;
padding: 0.8rem 1.5rem;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
color: var(--dark-purple);
font-size: large;
font-weight: 700;
}
.nav-item:hover {
background: var(--light-purple);
transform: translateY(-2px);
}
.nav-item:hover::after {
content: '';
position: absolute;
bottom: -5px;
left: 50%;
transform: translateX(-50%);
width: 80%;
height: 3px;
background: var(--primary-purple);
}
/* 内容容器 */
.content-container {
display: none;
max-width: 1300px;
margin: 2rem auto;
background: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
padding: 2rem;
}
.dashboard {
transform: scale(0.92);
display: flex;
margin-top: 30px;
gap: 20px;
max-width: 1800px;
margin: 20 auto;
}
/* 通用卡片样式 */
.card {
background: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
padding: 20px;
flex: 1;
min-height: 80vh;
}
h2 {
color: var(--dark-purple);
margin-bottom: 25px;
padding-bottom: 10px;
border-bottom: 2px solid var(--light-purple);
}
/* 日历区块 */
.calendar-container {
flex: 2;
min-width: 500px;
}
.calendar {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 5px;
margin-top: 10px;
}
.day-header {
background: var(--light-purple);
color: var(--dark-purple);
padding: 12px;
text-align: center;
border-radius: 8px;
}
.calendar-day {
min-height: 80px;
border: 1px solid var(--light-purple);
padding: 10px;
border-radius: 8px;
position: relative;
}
.day-number {
color: var(--secondary-purple);
font-weight: bold;
}
/* 待办事项 */
.todo-container {
flex: 1;
min-width: 300px;
}
#taskInput {
width: 100%;
padding: 12px;
border: 2px solid var(--light-purple);
border-radius: 8px;
margin-bottom: 15px;
margin-top: 10px;
}
.addBtn {
background: var(--primary-purple);
color: white;
padding: 12px;
border-radius: 12px;
cursor: pointer;
display: block;
text-align: center;
margin-bottom: 15px;
margin-top: 2px;
}
.close {
position: absolute;
right: 15px;
color: var(--primary-purple);
cursor: pointer;
}
/* 日程表单 */
.schedule-form {
flex: 1;
min-width: 300px;
}
.form-group {
margin-bottom: 15px;
margin-top: 10px;
}
input[type="date"],
input[type="time"],
input[type="text"] {
width: 100%;
padding: 10px;
border: 2px solid var(--light-purple);
border-radius: 8px;
}
.submit-btn {
background: var(--primary-purple);
color: white;
padding: 12px;
border: none;
border-radius: 8px;
margin-top: 8px;
width: 100%;
cursor: pointer;
}
@media (max-width: 1200px) {
.dashboard {
flex-wrap: wrap;
}
.calendar-container {
flex: 100%;
}
}
/* 任务列表 */
#taskList {
margin-top: 15px;
list-style: none;
padding: 0;
}
#taskList li {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 15px;
margin-bottom: 8px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
border-left: 4px solid #4a90e2;
}
#taskList li:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.task-text {
flex-grow: 1;
font-size: 16px;
color: #333;
padding-right: 10px;
word-break: break-word;
}
.completed {
text-decoration: line-through;
color: #95a5a6;
position: relative;
}
.completed::after {
content: "";
position: absolute;
left: 0;
top: 50%;
width: 100%;
height: 1px;
background: linear-gradient(90deg, rgba(149, 165, 166, 0.5), rgba(149, 165, 166, 0.2));
}
.task-actions {
display: flex;
gap: 8px;
}
.task-actions button {
width: 28px;
height: 28px;
border: none;
border-radius: 50%;
cursor: pointer;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.task-actions button:first-child {
background: #2ecc71;
color: white;
}
.task-actions button:last-child {
background: #e74c3c;
color: white;
}
.task-actions button:hover {
transform: scale(1.1);
}
/* 空状态提示 */
#taskList:empty::before {
content: "暂无待办事项,添加你的第一个任务吧~";
display: block;
text-align: center;
color: #95a5a6;
padding: 20px 0;
font-size: 14px;
}

+ 59
- 0
static/css/login_button.css View File

@ -0,0 +1,59 @@
.user-menu-box-container {
position: relative;
display: inline-block;
z-index: 100; /* ✅ 确保下拉在最上层 */
}
.user-menu-box-button {
display: flex;
align-items: center;
gap: 8px;
text-decoration: none;
color: #333;
padding: 6px 10px;
border-radius: 6px;
transition: background-color 0.2s ease;
cursor: pointer;
}
.user-menu-box-button:hover {
background-color: #f4f4f4;
}
.user-menu-box-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
object-fit: cover;
}
.user-menu-box-dropdown {
display: none;
position: absolute;
top: 100%;
right: 0;
min-width: 160px;
background: white;
border: 1px solid #ddd;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
z-index: 999; /* ✅ 确保悬浮框在其他元素上方 */
}
.user-menu-box-dropdown a {
display: block;
padding: 10px 15px;
color: #333;
text-decoration: none;
font-size: 14px;
white-space: nowrap;
transition: background-color 0.2s ease;
}
.user-menu-box-dropdown a:hover {
background-color: #f0f0f0;
}
.user-menu-box-container:hover .user-menu-box-dropdown {
display: block;
}

+ 931
- 0
static/css/myPage.css View File

@ -0,0 +1,931 @@
/* 全局样式 */
body {
font-family: Arial, sans-serif;
background-color: #f5f5f5;
margin: 0;
padding: 0;
}
/* 自定义背景 */
/* 左上角背景上传按钮样式 */
.bg-upload-container {
position: fixed;
top: 20px;
left: 10px;
background: rgba(0, 0, 0, 0.5);
padding: 8px 12px;
border-radius: 6px;
z-index: 1000;
}
.bg-upload-label {
color: white;
font-size: 14px;
cursor: pointer;
}
.bg-upload {
display: none;
}
/* 让背景图片自适应网页并平铺 */
body {
background-size: cover;
/* 确保图片适应整个屏幕 */
background-repeat: no-repeat;
/* 不重复 */
background-attachment: fixed;
/* 固定背景 */
background-position: center;
/* 居中对齐 */
}
/* 个人信息区域 */
/* 个人资料容器 */
.profile-container {
width: 100%;
max-width: 1000px;
background: white;
border-radius: 12px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
overflow: hidden;
text-align: center;
margin: 20px auto;
position: relative;
}
/* 背景横幅 */
.profile-banner {
height: 200px;
background: url("https://via.placeholder.com/800x200/7f7fff") center/cover no-repeat;
position: relative;
}
/* 更换背景按钮 */
.change-banner-btn {
position: absolute;
right: 10px;
bottom: 10px;
background: rgba(0, 0, 0, 0.5);
color: white;
border: none;
padding: 6px 12px;
border-radius: 5px;
cursor: pointer;
font-size: 12px;
}
.change-banner-btn:hover {
background: rgba(0, 0, 0, 0.7);
}
/* 头像容器 */
.profile-avatar-container {
position: absolute;
top: 120px;
/* 让头像悬浮在背景之上 */
left: 50%;
transform: translateX(-50%);
width: 130px;
height: 130px;
background: white;
/* 白色背景 */
border-radius: 50%;
border: 4px solid #ddd;
/* 灰色边框 */
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
/* 头像 */
.profile-avatar {
width: 120px;
height: 120px;
border-radius: 50%;
object-fit: cover;
cursor: pointer;
}
/* 个人信息部分 */
.profile-content {
padding-top: 60px;
/* 预留空间给悬浮的头像 */
}
/* 用户昵称 */
.profile-name {
font-size: 22px;
font-weight: bold;
color: #333;
}
/* 所在地 */
.profile-location {
font-size: 14px;
color: #666;
margin-bottom: 5px;
}
/* 个人签名 */
.profile-bio {
font-size: 14px;
color: #444;
padding: 0 15px;
margin-bottom: 10px;
}
/* 编辑资料按钮 */
.edit-profile-btn {
background: #BA68C8;
color: white;
padding: 10px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
margin-bottom: 15px;
font-size: 14px;
}
.edit-profile-btn:hover {
background: #9C4ABC;
}
/* 修改个人资料 */
/* 遮罩层 */
.modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
/* 加深遮罩,提高可读性 */
z-index: 9998;
}
/* 弹窗样式 */
.profile-modal {
display: none;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 400px;
background: #fff;
padding: 25px;
border-radius: 12px;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3);
z-index: 9999;
animation: fadeIn 0.3s ease-in-out;
}
/* 关闭按钮 */
.close-modal {
position: absolute;
top: 10px;
right: 15px;
font-size: 24px;
cursor: pointer;
color: #666;
}
.close-modal:hover {
color: #000;
}
/* 输入框样式 */
.profile-modal input,
.profile-modal textarea {
width: 100%;
padding: 10px;
margin-top: 5px;
margin-bottom: 15px;
border: 1px solid #ddd;
border-radius: 8px;
font-size: 14px;
}
/* 按钮样式 */
.update-profile-btn {
background: #9C4ABC;
color: white;
border: none;
padding: 12px;
border-radius: 8px;
width: 100%;
cursor: pointer;
font-size: 16px;
transition: background 0.2s;
}
.update-profile-btn:hover {
background: #9C4ABC;
}
/* 更新成功通知 */
.profile-update-notice {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
background: #4CAF50;
color: white;
padding: 12px 20px;
border-radius: 8px;
font-size: 16px;
display: none;
z-index: 10000;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
animation: fadeInOut 3s ease-in-out;
}
/* 动画效果 */
@keyframes fadeIn {
from {
opacity: 0;
transform: translate(-50%, -55%);
}
to {
opacity: 1;
transform: translate(-50%, -50%);
}
}
@keyframes fadeInOut {
0% {
opacity: 0;
transform: translate(-50%, -30px);
}
10% {
opacity: 1;
transform: translate(-50%, 0);
}
90% {
opacity: 1;
}
100% {
opacity: 0;
transform: translate(-50%, -30px);
}
}
/* 分栏布局 */
.main-content {
display: flex;
justify-content: space-between;
margin: 20px;
padding-left: 80px;
padding-right: 80px;
}
/* 左栏:好友列表 */
.left-column {
flex: 3;
margin: 5px;
padding: 10px;
background: #F3E5F5;
border-radius: 10px;
box-shadow: 0 6px 12px rgba(180, 136, 255, 0.2);
}
.left-column h3 {
text-align: center;
font-size: 20px;
margin-bottom: 10px;
color: #6A1B9A;
font-weight: bold;
}
/* 搜索框*/
.search-input {
width: 100%;
padding: 8px;
margin-bottom: 5px;
border-radius: 6px;
border: 1px solid #ddd;
font-size: 14px;
outline: none;
}
.search-input:focus {
border-color: #BA68C8;
}
/* 滑动好友列表容器 */
.friend-list-container {
display: flex;
flex-direction: column;
padding: 5px;
max-height: 250px;
/* 限制最大高度 */
overflow-y: auto;
/* 启用垂直滚动 */
margin-bottom: 15px;
}
/* 滚动条轨道 */
.friend-list-container::-webkit-scrollbar {
width: 8px;
/* 设置滚动条的宽度 */
}
/* 滚动条滑块 */
.friend-list-container::-webkit-scrollbar-thumb {
background-color: #BA68C8;
/* 滑块的颜色 */
border-radius: 4px;
/* 圆角 */
}
/* 分割线 */
.divider {
border: 1px solid #ffffff;
margin-top: 20px;
margin-bottom: 20px;
}
/* 好友分组标题 */
.friend-group {
margin-bottom: 5px;
}
.group-title {
display: flex;
justify-content: space-between;
font-size: 16px;
font-weight: bold;
color: #6A1B9A;
cursor: pointer;
padding: 5px 10px;
}
.group-title:hover {
color: #9C27B0;
}
.group-members {
display: none;
}
.group-members.show {
display: block;
}
/* 好友条目 */
.friend-item {
display: flex;
align-items: center;
background: white;
padding: 10px;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
margin-bottom: 5px;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.friend-item:hover {
transform: translateY(-4px);
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
}
/* 在线/离线状态 */
.friend-item::before {
content: "";
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 10px;
}
.friend-item.online::before {
background: green;
}
.friend-item.offline::before {
background: gray;
}
/* 头像样式 */
.friend-item img {
justify-self: left;
width: 40px;
height: 40px;
border-radius: 50%;
margin-right: 10px;
border: 2px solid #E1BEE7;
transition: transform 0.2s;
}
.friend-item img:hover {
transform: scale(1.1);
}
/* 文字部分 */
.friend-item span {
font-size: 14px;
font-weight: bold;
color: #5E35B1;
}
/* 私信按钮 */
.message-btn {
background: #BA68C8;
border: none;
color: white;
padding: 6px 8px;
border-radius: 6px;
cursor: pointer;
transition: background 0.3s;
margin-left: auto;
}
.message-btn:hover {
background: #9C27B0;
}
.message-btn i {
font-size: 16px;
}
/* 中栏:帖子区 */
.middle-column {
flex: 6;
margin: 5px;
padding: 10px;
background: #F3E5F5;
border-radius: 6px;
box-shadow: 0 4px 8px rgba(180, 136, 255, 0.2);
}
/* Tab容器样式 */
.tab {
display: flex;
justify-content: space-evenly;
align-items: center;
background-color: #ffffff;
padding: 5px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin: 0 auto;
}
/* Tab按钮样式 */
.tablinks {
background-color: transparent;
color: #6a1b9a;
border: none;
padding: 10px;
font-size: 16px;
cursor: pointer;
text-align: left;
/* 左对齐文字 */
display: flex;
/* 使用flex布局 */
align-items: center;
/* 垂直居中对齐内容 */
gap: 10px;
/* 图标和文字之间的间距 */
transition: all 0.3s ease;
margin: 5px;
font-weight: 500;
}
/* 鼠标悬停时的样式 */
.tablinks:hover {
background-color: #f3e5f5;
color: #6a1b9a;
transform: translateY(-2px);
/* 鼠标悬停时按钮稍微上浮 */
}
/* 激活状态的样式 */
.tablinks.active {
border-bottom: 3px solid #6a1b9a;
background-color: #f3e5f5;
color: #6a1b9a;
font-weight: bold;
transform: translateY(-2px);
/* 激活状态时按钮稍微上浮 */
}
/* Tab内容区域 */
.tab-content {
display: flex;
align-items: center;
}
.tab-content p {
font-size: 16px;
margin-left: 5px;
}
/* 选项卡内容 */
.tabcontent {
display: none;
padding: 10px;
}
#myPosts {
display: block;
}
/* 让所有 tab 下的帖子容器都适用 */
.postContainer {
display: flex;
flex-direction: column;
/* 帖子纵向排列 */
gap: 10px;
/* 帖子之间留间距 */
align-items: center;
/* 居中对齐 */
}
/* 单个帖子样式 */
.feed-post {
width: 100%;
max-width: 600px;
margin: 0 auto;
padding: 15px;
border-radius: 8px;
background-color: white;
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.1);
transition: box-shadow 0.3s ease-in-out;
}
.feed-post:hover {
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
}
/* 帖子头部对齐 */
.post-header {
display: flex;
align-items: center;
gap: 10px;
}
.post-avatar img {
width: 50px;
height: 50px;
border-radius: 50%;
}
.post-info {
font-size: 14px;
color: gray;
}
.post-user-name {
font-size: 16px;
font-weight: bold;
color: #6A1B9A;
}
.post-time {
font-size: 12px;
color: gray;
}
/* 帖子正文 */
.post-content {
font-size: 16px;
color: #333;
margin: 10px 0;
line-height: 1.5;
}
/* 帖子操作按钮 */
/* 右上角扩展栏 */
.post-options {
position: relative;
margin-left: auto;
cursor: pointer;
}
.post-options i {
font-size: 18px;
color: #888;
transition: color 0.3s;
}
.post-options i:hover {
color: #555;
}
/* 🔹 扩展菜单 */
.options-menu {
position: absolute;
right: 0;
top: 25px;
background: white;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.12); /* 柔和阴影 */
border-radius: 6px;
display: none;
flex-direction: column;
min-width: 120px; /* 设置最小宽度 */
width: auto; /* 宽度根据内容自适应 */
padding: 5px 0; /* 让菜单项不会贴着边界 */
overflow: hidden; /* 避免内容溢出 */
}
/* 🔹 菜单项 */
.options-menu button {
background: none;
border: none;
padding: 8px 15px; /* 适当增加左右间距 */
text-align: left;
width: 100%;
cursor: pointer;
font-size: 14px;
color: #4A148C; /* 深紫色,符合整体风格 */
white-space: nowrap; /* 防止换行 */
transition: background 0.2s, color 0.2s;
}
.options-menu button i{
color: #4A148C;
margin-right: 5px;
}
/* 🔹 悬浮时的美化 */
.options-menu button:hover {
background: #f3e5f5; /* 柔和的淡紫色背景 */
color: #6A1B9A; /* 略深的紫色文本 */
}
.options-menu button:hover {
background: #f3e5f5;
}
/* 🔹 帖子交互区域 */
.post-actions {
display: flex;
justify-content: space-between;
gap: 10px;
margin-top: 10px;
padding-top: 8px;
border-top: 1px solid #e0cce5; /* 添加分隔线,使交互区域更融合 */
}
/* 🔹 交互按钮美化 */
.post-actions button {
background: none; /* 默认无背景,使其更自然 */
color: #8e44ad; /* 低饱和度紫色 */
border: none;
padding: 4px 8px;
font-size: 14px;
cursor: pointer;
display: flex;
align-items: center;
gap: 5px;
transition: color 0.3s, transform 0.2s;
}
/* 🔹 悬浮时增加互动感 */
.post-actions button:hover {
color: #6a1b9a; /* 悬浮时颜色稍深 */
transform: scale(1.05); /* 悬浮时略微放大 */
}
/* 🔹 按钮内图标样式 */
.post-actions button i {
font-size: 16px;
transition: color 0.3s;
}
/* 🔹 按钮悬浮时图标颜色变化 */
.post-actions button:hover i {
color: #6a1b9a;
}
/* 分页容器 */
.pagination {
display: flex;
justify-content: center;
align-items: center;
margin-top: 15px;
gap: 8px;
}
/* 分页按钮 */
.pagination button {
padding: 8px 12px;
border: none;
background: #BA68C8;
color: white;
border-radius: 5px;
cursor: pointer;
transition: background 0.3s;
}
.pagination button:hover {
background: #9C27B0;
}
/* 禁用按钮样式 */
.pagination button:disabled {
background: #D1C4E9;
cursor: not-allowed;
}
/* 分页信息 */
.page-info {
font-size: 14px;
color: #666;
}
/* 页码输入框 */
.page-input {
width: 50px;
padding: 6px;
border: 1px solid #ccc;
border-radius: 4px;
text-align: center;
}
.page-input:focus {
outline: 2px solid #BA68C8;
}
/* 右栏:通知区 */
/* 🎯 右栏整体布局 */
.right-column {
flex: 3.5;
margin: 5px;
padding: 10px;
background: #F3E5F5;
border-radius: 6px;
box-shadow: 0 4px 8px rgba(180, 136, 255, 0.2);
}
/* 📌 通知卡片基础样式 */
.notification-card {
background: linear-gradient(135deg, #ffffff 0%, #f9f9f9 100%);
padding: 15px;
margin-bottom: 15px;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.08);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.notification-card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.12);
}
.notification-card h4 {
font-size: 18px;
margin-bottom: 10px;
}
/* 📅 日程管理 */
.todo-list {
list-style: none;
padding: 0;
}
.todo-list li {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
margin-bottom: 6px;
cursor: pointer;
transition: color 0.2s ease;
}
.todo-list li:hover {
color: #6A1B9A;
}
/* 🏫 手语教室按钮 */
.join-class-btn {
background: #6A1B9A;
color: white;
border: none;
padding: 8px 12px;
border-radius: 6px;
cursor: pointer;
margin-top: 10px;
transition: background 0.3s ease, transform 0.2s ease;
}
.join-class-btn:hover {
background: #5C1380;
transform: scale(1.05);
}
/* 🔔 互动通知 */
.notifications ul {
list-style: none;
padding: 0;
}
.notifications li {
display: flex;
align-items: center;
gap: 10px;
font-size: 14px;
margin-bottom: 6px;
padding: 6px 8px;
border-radius: 6px;
transition: background 0.2s ease;
cursor: pointer;
}
.notifications li:hover {
background: rgba(106, 27, 154, 0.08);
}
.notifications img {
width: 30px;
height: 30px;
border-radius: 50%;
}
/* 🌟 个性化推荐 */
.recommendations p {
background: rgba(186, 104, 200, 0.15);
padding: 8px 10px;
border-radius: 6px;
margin: 5px 0;
font-size: 14px;
transition: background 0.3s ease;
cursor: pointer;
}
.recommendations p:hover {
background: rgba(186, 104, 200, 0.3);
}
/* 🏆 每日签到 */
.checkin-btn {
background: #BA68C8;
color: white;
border: none;
padding: 6px 12px;
border-radius: 5px;
cursor: pointer;
display: block;
margin-top: 5px;
transition: background 0.3s ease, transform 0.2s ease;
}
.checkin-btn:hover {
background: #9C27B0;
transform: scale(1.05);
}
/* 🎖️ 勋章墙 */
.badge-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 10px;
}
.badge {
background: rgba(186, 104, 200, 0.2);
padding: 5px 10px;
border-radius: 20px;
font-size: 12px;
color: #6A1B9A;
}

+ 216
- 0
static/data/你好.json View File

@ -0,0 +1,216 @@
[
[
{
"x": 0.6320064067840576,
"y": 0.6639246344566345,
"z": -2.3567096718579705e-8
},
{
"x": 0.6277491450309753,
"y": 0.5509824156761169,
"z": -0.02596704475581646
},
{
"x": 0.6014074683189392,
"y": 0.46007394790649414,
"z": -0.05003476142883301
},
{
"x": 0.5511794090270996,
"y": 0.45773810148239136,
"z": -0.07211336493492126
},
{
"x": 0.5334982872009277,
"y": 0.5253194570541382,
"z": -0.09125160425901413
},
{
"x": 0.6521157026290894,
"y": 0.48740556836128235,
"z": -0.06543701142072678
},
{
"x": 0.6120480895042419,
"y": 0.44475099444389343,
"z": -0.09859855473041534
},
{
"x": 0.6045635938644409,
"y": 0.4603981375694275,
"z": -0.11079375445842743
},
{
"x": 0.618733823299408,
"y": 0.47828903794288635,
"z": -0.1165243610739708
},
{
"x": 0.6504209637641907,
"y": 0.5616504549980164,
"z": -0.07005614787340164
},
{
"x": 0.5382657647132874,
"y": 0.5441007614135742,
"z": -0.098802350461483
},
{
"x": 0.5524492859840393,
"y": 0.5471982359886169,
"z": -0.0959862768650055
},
{
"x": 0.5857524871826172,
"y": 0.5444830060005188,
"z": -0.0920417457818985
},
{
"x": 0.6375548839569092,
"y": 0.639783501625061,
"z": -0.07619240134954453
},
{
"x": 0.5259181261062622,
"y": 0.6331387758255005,
"z": -0.09718460589647293
},
{
"x": 0.5531055927276611,
"y": 0.6360833048820496,
"z": -0.0775383710861206
},
{
"x": 0.595370352268219,
"y": 0.6322951316833496,
"z": -0.059992071241140366
},
{
"x": 0.6223289966583252,
"y": 0.7055690884590149,
"z": -0.08569794148206711
},
{
"x": 0.5446730852127075,
"y": 0.6974737644195557,
"z": -0.10043548792600632
},
{
"x": 0.5667818188667297,
"y": 0.6949626207351685,
"z": -0.08594515919685364
},
{
"x": 0.6028042435646057,
"y": 0.6969892382621765,
"z": -0.07056321948766708
}
],
[
{
"x": 0.6159699559211731,
"y": 0.8369808793067932,
"z": -3.8672851587762125e-7
},
{
"x": 0.6080248951911926,
"y": 0.713093638420105,
"z": -0.029090309515595436
},
{
"x": 0.5984982252120972,
"y": 0.5673580169677734,
"z": -0.04789982736110687
},
{
"x": 0.6059970259666443,
"y": 0.45955491065979004,
"z": -0.061475686728954315
},
{
"x": 0.6314979791641235,
"y": 0.3899309039115906,
"z": -0.0721668154001236
},
{
"x": 0.5945241451263428,
"y": 0.6108418703079224,
"z": -0.07783110439777374
},
{
"x": 0.5036718249320984,
"y": 0.5679571032524109,
"z": -0.12085898965597153
},
{
"x": 0.5202652812004089,
"y": 0.6145290732383728,
"z": -0.13886433839797974
},
{
"x": 0.5580585598945618,
"y": 0.6238594651222229,
"z": -0.1484847217798233
},
{
"x": 0.5865462422370911,
"y": 0.7028706073760986,
"z": -0.08085721731185913
},
{
"x": 0.4799419045448303,
"y": 0.6708379983901978,
"z": -0.11611701548099518
},
{
"x": 0.5056391358375549,
"y": 0.7051485180854797,
"z": -0.11365342885255814
},
{
"x": 0.5432829856872559,
"y": 0.7081708312034607,
"z": -0.1135244220495224
},
{
"x": 0.5734222531318665,
"y": 0.7860034704208374,
"z": -0.08634401112794876
},
{
"x": 0.48193442821502686,
"y": 0.7633798122406006,
"z": -0.11750930547714233
},
{
"x": 0.5072769522666931,
"y": 0.7826641201972961,
"z": -0.10152815282344818
},
{
"x": 0.546349287033081,
"y": 0.7849104404449463,
"z": -0.09001252055168152
},
{
"x": 0.5629417896270752,
"y": 0.8544789552688599,
"z": -0.0937301442027092
},
{
"x": 0.4911764860153198,
"y": 0.8383667469024658,
"z": -0.1155904158949852
},
{
"x": 0.5122630596160889,
"y": 0.8497434854507446,
"z": -0.10698479413986206
},
{
"x": 0.5474485754966736,
"y": 0.8557199239730835,
"z": -0.09859214723110199
}
]
]

+ 323
- 0
static/data/早上好.json View File

@ -0,0 +1,323 @@
[
[
{
"x": 0.5821961760520935,
"y": 0.4449542164802551,
"z": -9.344361160401604e-7
},
{
"x": 0.5408991575241089,
"y": 0.3645845055580139,
"z": -0.007521727588027716
},
{
"x": 0.49161839485168457,
"y": 0.3183490037918091,
"z": -0.021221067756414413
},
{
"x": 0.42823928594589233,
"y": 0.32109713554382324,
"z": -0.03742992505431175
},
{
"x": 0.374701589345932,
"y": 0.3474753499031067,
"z": -0.05756435915827751
},
{
"x": 0.5522622466087341,
"y": 0.268653005361557,
"z": -0.027303414419293404
},
{
"x": 0.46853742003440857,
"y": 0.30156755447387695,
"z": -0.04382767528295517
},
{
"x": 0.4176906943321228,
"y": 0.32629409432411194,
"z": -0.05896339938044548
},
{
"x": 0.39042484760284424,
"y": 0.33372417092323303,
"z": -0.0706745982170105
},
{
"x": 0.566277265548706,
"y": 0.33881819248199463,
"z": -0.04086381942033768
},
{
"x": 0.45396167039871216,
"y": 0.38698825240135193,
"z": -0.04755357652902603
},
{
"x": 0.40190404653549194,
"y": 0.4009895920753479,
"z": -0.05840885266661644
},
{
"x": 0.37351930141448975,
"y": 0.4007323384284973,
"z": -0.0743001326918602
},
{
"x": 0.5683354139328003,
"y": 0.41107070446014404,
"z": -0.05355175584554672
},
{
"x": 0.45562678575515747,
"y": 0.45991820096969604,
"z": -0.062346797436475754
},
{
"x": 0.40588200092315674,
"y": 0.4656679630279541,
"z": -0.0669768825173378
},
{
"x": 0.3785693049430847,
"y": 0.4577176868915558,
"z": -0.07633139193058014
},
{
"x": 0.5607712268829346,
"y": 0.47670793533325195,
"z": -0.06756220012903214
},
{
"x": 0.47159236669540405,
"y": 0.524438202381134,
"z": -0.07291802763938904
},
{
"x": 0.4328312873840332,
"y": 0.5266100168228149,
"z": -0.0667572095990181
},
{
"x": 0.4060453772544861,
"y": 0.5178602337837219,
"z": -0.0656297355890274
}
],
[
{
"x": 0.6141078472137451,
"y": 0.6236022710800171,
"z": 6.703137387376046e-7
},
{
"x": 0.5680003762245178,
"y": 0.6110613942146301,
"z": -0.023212945088744164
},
{
"x": 0.5315503478050232,
"y": 0.5757322311401367,
"z": -0.04985279217362404
},
{
"x": 0.5001187324523926,
"y": 0.5689186453819275,
"z": -0.07786590605974197
},
{
"x": 0.4662664234638214,
"y": 0.5739880204200745,
"z": -0.10950223356485367
},
{
"x": 0.5384328365325928,
"y": 0.40875574946403503,
"z": -0.0495375357568264
},
{
"x": 0.5277424454689026,
"y": 0.29326391220092773,
"z": -0.08207526803016663
},
{
"x": 0.5212165117263794,
"y": 0.21330133080482483,
"z": -0.10592856258153915
},
{
"x": 0.517288088798523,
"y": 0.14644545316696167,
"z": -0.1231740415096283
},
{
"x": 0.5493270754814148,
"y": 0.4067770540714264,
"z": -0.061399269849061966
},
{
"x": 0.5365923047065735,
"y": 0.2741358280181885,
"z": -0.09309384971857071
},
{
"x": 0.5305213332176208,
"y": 0.18132558465003967,
"z": -0.11716707795858383
},
{
"x": 0.528063952922821,
"y": 0.09768706560134888,
"z": -0.13444815576076508
},
{
"x": 0.5639584064483643,
"y": 0.42088162899017334,
"z": -0.07484324276447296
},
{
"x": 0.5528886318206787,
"y": 0.29208576679229736,
"z": -0.10330238938331604
},
{
"x": 0.545394778251648,
"y": 0.2104230523109436,
"z": -0.12457089871168137
},
{
"x": 0.5377002954483032,
"y": 0.13965079188346863,
"z": -0.13888947665691376
},
{
"x": 0.5833427309989929,
"y": 0.44702526926994324,
"z": -0.09025055915117264
},
{
"x": 0.5803030729293823,
"y": 0.3531721532344818,
"z": -0.1144757866859436
},
{
"x": 0.5725305676460266,
"y": 0.2919052243232727,
"z": -0.12837167084217072
},
{
"x": 0.5623204708099365,
"y": 0.23511989414691925,
"z": -0.13815775513648987
}
],
[
{
"x": 0.6159699559211731,
"y": 0.8369808793067932,
"z": -3.8672851587762125e-7
},
{
"x": 0.6080248951911926,
"y": 0.713093638420105,
"z": -0.029090309515595436
},
{
"x": 0.5984982252120972,
"y": 0.5673580169677734,
"z": -0.04789982736110687
},
{
"x": 0.6059970259666443,
"y": 0.45955491065979004,
"z": -0.061475686728954315
},
{
"x": 0.6314979791641235,
"y": 0.3899309039115906,
"z": -0.0721668154001236
},
{
"x": 0.5945241451263428,
"y": 0.6108418703079224,
"z": -0.07783110439777374
},
{
"x": 0.5036718249320984,
"y": 0.5679571032524109,
"z": -0.12085898965597153
},
{
"x": 0.5202652812004089,
"y": 0.6145290732383728,
"z": -0.13886433839797974
},
{
"x": 0.5580585598945618,
"y": 0.6238594651222229,
"z": -0.1484847217798233
},
{
"x": 0.5865462422370911,
"y": 0.7028706073760986,
"z": -0.08085721731185913
},
{
"x": 0.4799419045448303,
"y": 0.6708379983901978,
"z": -0.11611701548099518
},
{
"x": 0.5056391358375549,
"y": 0.7051485180854797,
"z": -0.11365342885255814
},
{
"x": 0.5432829856872559,
"y": 0.7081708312034607,
"z": -0.1135244220495224
},
{
"x": 0.5734222531318665,
"y": 0.7860034704208374,
"z": -0.08634401112794876
},
{
"x": 0.48193442821502686,
"y": 0.7633798122406006,
"z": -0.11750930547714233
},
{
"x": 0.5072769522666931,
"y": 0.7826641201972961,
"z": -0.10152815282344818
},
{
"x": 0.546349287033081,
"y": 0.7849104404449463,
"z": -0.09001252055168152
},
{
"x": 0.5629417896270752,
"y": 0.8544789552688599,
"z": -0.0937301442027092
},
{
"x": 0.4911764860153198,
"y": 0.8383667469024658,
"z": -0.1155904158949852
},
{
"x": 0.5122630596160889,
"y": 0.8497434854507446,
"z": -0.10698479413986206
},
{
"x": 0.5474485754966736,
"y": 0.8557199239730835,
"z": -0.09859214723110199
}
]
]

+ 430
- 0
static/data/谢谢.json View File

@ -0,0 +1,430 @@
[
[
{
"x": 0.5766940712928772,
"y": 0.6520374417304993,
"z": -5.891508862987394e-7
},
{
"x": 0.55990070104599,
"y": 0.564600944519043,
"z": -0.041584618389606476
},
{
"x": 0.5691367983818054,
"y": 0.4611101448535919,
"z": -0.06258521229028702
},
{
"x": 0.5788955092430115,
"y": 0.3869872987270355,
"z": -0.07633058726787567
},
{
"x": 0.582440197467804,
"y": 0.31022900342941284,
"z": -0.0848177894949913
},
{
"x": 0.6187899112701416,
"y": 0.5000607967376709,
"z": -0.06497752666473389
},
{
"x": 0.5407440662384033,
"y": 0.5068030953407288,
"z": -0.1019575223326683
},
{
"x": 0.5392554998397827,
"y": 0.5241277813911438,
"z": -0.12286828458309174
},
{
"x": 0.5672339200973511,
"y": 0.5229101777076721,
"z": -0.1368086040019989
},
{
"x": 0.6329519152641296,
"y": 0.5783523321151733,
"z": -0.06159749999642372
},
{
"x": 0.5314179062843323,
"y": 0.5986599922180176,
"z": -0.09116323292255402
},
{
"x": 0.5409641861915588,
"y": 0.602307915687561,
"z": -0.0961412638425827
},
{
"x": 0.5735096335411072,
"y": 0.5913326740264893,
"z": -0.10176314413547516
},
{
"x": 0.6320325136184692,
"y": 0.6550064086914062,
"z": -0.06200835481286049
},
{
"x": 0.5383253693580627,
"y": 0.6733283996582031,
"z": -0.09224426746368408
},
{
"x": 0.5453531742095947,
"y": 0.6689015030860901,
"z": -0.08138420432806015
},
{
"x": 0.5751619935035706,
"y": 0.6548710465431213,
"z": -0.07285572588443756
},
{
"x": 0.6205134391784668,
"y": 0.7178999185562134,
"z": -0.06690721958875656
},
{
"x": 0.5517123341560364,
"y": 0.7373326420783997,
"z": -0.09246259927749634
},
{
"x": 0.5568342804908752,
"y": 0.7287055253982544,
"z": -0.09025078266859055
},
{
"x": 0.5817087292671204,
"y": 0.719586968421936,
"z": -0.08630824089050293
}
],
[
{
"x": 0.5766940712928772,
"y": 0.6520374417304993,
"z": -5.891508862987394e-7
},
{
"x": 0.55990070104599,
"y": 0.564600944519043,
"z": -0.041584618389606476
},
{
"x": 0.5691367983818054,
"y": 0.4611101448535919,
"z": -0.06258521229028702
},
{
"x": 0.5788955092430115,
"y": 0.3869872987270355,
"z": -0.07633058726787567
},
{
"x": 0.582440197467804,
"y": 0.41022900342941284,
"z": 0.3048177894949913
},
{
"x": 0.6187899112701416,
"y": 0.5000607967376709,
"z": -0.06497752666473389
},
{
"x": 0.5407440662384033,
"y": 0.5068030953407288,
"z": -0.1019575223326683
},
{
"x": 0.5392554998397827,
"y": 0.5241277813911438,
"z": -0.12286828458309174
},
{
"x": 0.5672339200973511,
"y": 0.5229101777076721,
"z": -0.1368086040019989
},
{
"x": 0.6329519152641296,
"y": 0.5783523321151733,
"z": -0.06159749999642372
},
{
"x": 0.5314179062843323,
"y": 0.5986599922180176,
"z": -0.09116323292255402
},
{
"x": 0.5409641861915588,
"y": 0.602307915687561,
"z": -0.0961412638425827
},
{
"x": 0.5735096335411072,
"y": 0.5913326740264893,
"z": -0.10176314413547516
},
{
"x": 0.6320325136184692,
"y": 0.6550064086914062,
"z": -0.06200835481286049
},
{
"x": 0.5383253693580627,
"y": 0.6733283996582031,
"z": -0.09224426746368408
},
{
"x": 0.5453531742095947,
"y": 0.6689015030860901,
"z": -0.08138420432806015
},
{
"x": 0.5751619935035706,
"y": 0.6548710465431213,
"z": -0.07285572588443756
},
{
"x": 0.6205134391784668,
"y": 0.7178999185562134,
"z": -0.06690721958875656
},
{
"x": 0.5517123341560364,
"y": 0.7373326420783997,
"z": -0.09246259927749634
},
{
"x": 0.5568342804908752,
"y": 0.7287055253982544,
"z": -0.09025078266859055
},
{
"x": 0.5817087292671204,
"y": 0.719586968421936,
"z": -0.08630824089050293
}
],
[
{
"x": 0.5766940712928772,
"y": 0.6520374417304993,
"z": -5.891508862987394e-7
},
{
"x": 0.55990070104599,
"y": 0.564600944519043,
"z": -0.041584618389606476
},
{
"x": 0.5691367983818054,
"y": 0.4611101448535919,
"z": -0.06258521229028702
},
{
"x": 0.5788955092430115,
"y": 0.3869872987270355,
"z": -0.07633058726787567
},
{
"x": 0.582440197467804,
"y": 0.31022900342941284,
"z": -0.0848177894949913
},
{
"x": 0.6187899112701416,
"y": 0.5000607967376709,
"z": -0.06497752666473389
},
{
"x": 0.5407440662384033,
"y": 0.5068030953407288,
"z": -0.1019575223326683
},
{
"x": 0.5392554998397827,
"y": 0.5241277813911438,
"z": -0.12286828458309174
},
{
"x": 0.5672339200973511,
"y": 0.5229101777076721,
"z": -0.1368086040019989
},
{
"x": 0.6329519152641296,
"y": 0.5783523321151733,
"z": -0.06159749999642372
},
{
"x": 0.5314179062843323,
"y": 0.5986599922180176,
"z": -0.09116323292255402
},
{
"x": 0.5409641861915588,
"y": 0.602307915687561,
"z": -0.0961412638425827
},
{
"x": 0.5735096335411072,
"y": 0.5913326740264893,
"z": -0.10176314413547516
},
{
"x": 0.6320325136184692,
"y": 0.6550064086914062,
"z": -0.06200835481286049
},
{
"x": 0.5383253693580627,
"y": 0.6733283996582031,
"z": -0.09224426746368408
},
{
"x": 0.5453531742095947,
"y": 0.6689015030860901,
"z": -0.08138420432806015
},
{
"x": 0.5751619935035706,
"y": 0.6548710465431213,
"z": -0.07285572588443756
},
{
"x": 0.6205134391784668,
"y": 0.7178999185562134,
"z": -0.06690721958875656
},
{
"x": 0.5517123341560364,
"y": 0.7373326420783997,
"z": -0.09246259927749634
},
{
"x": 0.5568342804908752,
"y": 0.7287055253982544,
"z": -0.09025078266859055
},
{
"x": 0.5817087292671204,
"y": 0.719586968421936,
"z": -0.08630824089050293
}
],
[
{
"x": 0.5766940712928772,
"y": 0.6520374417304993,
"z": -5.891508862987394e-7
},
{
"x": 0.55990070104599,
"y": 0.564600944519043,
"z": -0.041584618389606476
},
{
"x": 0.5691367983818054,
"y": 0.4611101448535919,
"z": -0.06258521229028702
},
{
"x": 0.5788955092430115,
"y": 0.3869872987270355,
"z": -0.07633058726787567
},
{
"x": 0.582440197467804,
"y": 0.41022900342941284,
"z": 0.3048177894949913
},
{
"x": 0.6187899112701416,
"y": 0.5000607967376709,
"z": -0.06497752666473389
},
{
"x": 0.5407440662384033,
"y": 0.5068030953407288,
"z": -0.1019575223326683
},
{
"x": 0.5392554998397827,
"y": 0.5241277813911438,
"z": -0.12286828458309174
},
{
"x": 0.5672339200973511,
"y": 0.5229101777076721,
"z": -0.1368086040019989
},
{
"x": 0.6329519152641296,
"y": 0.5783523321151733,
"z": -0.06159749999642372
},
{
"x": 0.5314179062843323,
"y": 0.5986599922180176,
"z": -0.09116323292255402
},
{
"x": 0.5409641861915588,
"y": 0.602307915687561,
"z": -0.0961412638425827
},
{
"x": 0.5735096335411072,
"y": 0.5913326740264893,
"z": -0.10176314413547516
},
{
"x": 0.6320325136184692,
"y": 0.6550064086914062,
"z": -0.06200835481286049
},
{
"x": 0.5383253693580627,
"y": 0.6733283996582031,
"z": -0.09224426746368408
},
{
"x": 0.5453531742095947,
"y": 0.6689015030860901,
"z": -0.08138420432806015
},
{
"x": 0.5751619935035706,
"y": 0.6548710465431213,
"z": -0.07285572588443756
},
{
"x": 0.6205134391784668,
"y": 0.7178999185562134,
"z": -0.06690721958875656
},
{
"x": 0.5517123341560364,
"y": 0.7373326420783997,
"z": -0.09246259927749634
},
{
"x": 0.5568342804908752,
"y": 0.7287055253982544,
"z": -0.09025078266859055
},
{
"x": 0.5817087292671204,
"y": 0.719586968421936,
"z": -0.08630824089050293
}
]
]

BIN
static/images/1.png View File

Before After
Width: 683  |  Height: 238  |  Size: 167 KiB

BIN
static/images/2.jpg View File

Before After
Width: 1170  |  Height: 779  |  Size: 759 KiB

BIN
static/images/3.jpg View File

Before After
Width: 1851  |  Height: 994  |  Size: 166 KiB

BIN
static/images/OIP-C.jpg View File

Before After
Width: 474  |  Height: 474  |  Size: 13 KiB

BIN
static/images/community_video.mp4 View File


BIN
static/images/default_avatar.png View File

Before After
Width: 554  |  Height: 560  |  Size: 45 KiB

BIN
static/images/item1.png View File

Before After
Width: 681  |  Height: 493  |  Size: 481 KiB

BIN
static/images/item2.png View File

Before After
Width: 637  |  Height: 595  |  Size: 413 KiB

BIN
static/images/item3.png View File

Before After
Width: 411  |  Height: 327  |  Size: 119 KiB

BIN
static/images/mapgoing.jpg View File

Before After
Width: 900  |  Height: 383  |  Size: 81 KiB

BIN
static/images/profile/baking.jpg View File

Before After
Width: 860  |  Height: 573  |  Size: 126 KiB

BIN
static/images/profile/family.jpg View File

Before After
Width: 800  |  Height: 600  |  Size: 210 KiB

BIN
static/images/profile/game.jpg View File

Before After
Width: 2048  |  Height: 1363  |  Size: 1.7 MiB

BIN
static/images/profile/travel.jpg View File

Before After
Width: 1080  |  Height: 720  |  Size: 138 KiB

BIN
static/images/profile/tree.jpeg View File

Before After
Width: 1200  |  Height: 800  |  Size: 386 KiB

BIN
static/images/profile2.png View File

Before After
Width: 557  |  Height: 547  |  Size: 202 KiB

BIN
static/images/profile3.png View File

Before After
Width: 692  |  Height: 543  |  Size: 749 KiB

BIN
static/images/proflie1.png View File

Before After
Width: 641  |  Height: 547  |  Size: 660 KiB

BIN
static/images/readme/手势识别1.png View File

Before After
Width: 2816  |  Height: 1348  |  Size: 401 KiB

BIN
static/images/readme/手势识别2.png View File

Before After
Width: 1323  |  Height: 1458  |  Size: 1.1 MiB

BIN
static/images/recommendGoods.jpg View File

Before After
Width: 1125  |  Height: 633  |  Size: 140 KiB

BIN
static/images/recruit.jpg View File

Before After
Width: 900  |  Height: 383  |  Size: 76 KiB

BIN
static/images/sign_good.png View File

Before After
Width: 178  |  Height: 234  |  Size: 58 KiB

BIN
static/images/sign_thank_you.png View File

Before After
Width: 207  |  Height: 206  |  Size: 62 KiB

BIN
static/images/sign_you.png View File

Before After
Width: 166  |  Height: 197  |  Size: 54 KiB

BIN
static/images/登录1.jpg View File

Before After
Width: 1114  |  Height: 738  |  Size: 532 KiB

BIN
static/images/聊天2.jpg View File

Before After
Width: 236  |  Height: 236  |  Size: 5.5 KiB

BIN
static/images/聊天头像2.jpg View File

Before After
Width: 209  |  Height: 228  |  Size: 5.9 KiB

+ 71
- 0
static/js/SLClassroom.js View File

@ -0,0 +1,71 @@
function openTab(evt, tabName) {
var i, tabcontent, tablinks;
tabcontent = document.getElementsByClassName("tabcontent");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none"; // 隐藏所有选项卡内容
}
tablinks = document.getElementsByClassName("tablinks");
for (i = 0; i < tablinks.length; i++) {
tablinks[i].className = tablinks[i].className.replace(" active", ""); // 移除活动状态
}
document.getElementById(tabName).style.display = "block"; // 显示当前选中的内容
evt.currentTarget.className += " active"; // 给当前按钮添加活动状态
}
// 动画监听绑定
document.querySelectorAll('.task-item').forEach(item => {
item.addEventListener('mouseenter', () => {
const bonus = item.querySelector('.bonus');
bonus.classList.add('show'); // 显示积分动画
});
item.addEventListener('mouseleave', () => {
const bonus = item.querySelector('.bonus');
bonus.classList.remove('show'); // 隐藏积分动画
});
});
function flipCard(card) {
card.classList.toggle('flipped');
}
function openLesson(lessonNumber) {
alert("你正在学习:" + lessonNumber + " 号课程");
}
// ✅ 页面加载后再执行默认 tab 和视频绑定
document.addEventListener("DOMContentLoaded", function () {
// 👉 默认点击 tab
const defaultBtn = document.getElementById("defaultOpen");
if (defaultBtn) {
defaultBtn.click();
} else {
console.warn("#defaultOpen not found");
}
// 👉 视频绑定
const standardVideo = document.getElementById("standardVideo");
const gestureSelect = document.getElementById("gestureSelect");
if (gestureSelect && standardVideo) {
const videoMap = {
"你好": "/static/videos/你好.mp4",
"谢谢": "/static/videos/谢谢.mp4",
"早上好": "/static/videos/早上好.mp4"
};
// 默认设置一次
standardVideo.src = videoMap[gestureSelect.value];
standardVideo.load();
// 每次切换时更新
gestureSelect.addEventListener("change", function () {
const selectedGesture = gestureSelect.value;
if (videoMap[selectedGesture]) {
standardVideo.src = videoMap[selectedGesture];
standardVideo.load();
}
});
}
});

+ 2
- 0
static/js/mediapipe/vision_bundle.mjs
File diff suppressed because it is too large
View File


+ 22
- 0
static/js/mediapipe/wasm/vision_wasm_internal.js
File diff suppressed because it is too large
View File


BIN
static/js/mediapipe/wasm/vision_wasm_internal.wasm View File


BIN
static/js/mediapipe/wasm/vision_wasm_nosimd_internal.wasm View File


+ 625
- 0
static/js/myPage.js View File

@ -0,0 +1,625 @@
// 头像
function changeAvatar(event) {
const input = event.target;
const file = input.files[0];
if (file) {
// 预览
const reader = new FileReader();
reader.onload = function (e) {
document.getElementById("profileAvatar").src = e.target.result;
};
reader.readAsDataURL(file);
// 实时上传
const formData = new FormData();
formData.append("avatar", file);
fetch("/MyPage/upload-avatar/", {
method: "POST",
headers: {
"X-CSRFToken": getCookie("csrftoken")
},
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
console.log("头像上传成功");
} else {
console.error("上传失败:", data.message);
}
})
.catch(error => {
console.error("请求错误:", error);
});
}
}
// 工具函数:获取 CSRF token
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== "") {
const cookies = document.cookie.split(";");
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === name + "=") {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
// 个人资料背景图
function changeBanner(event) {
const input = event.target;
const file = input.files[0];
if (file) {
// 本地预览
const reader = new FileReader();
reader.onload = function (e) {
document.getElementById("profileBanner").style.backgroundImage = `url('${e.target.result}')`;
};
reader.readAsDataURL(file);
// 实时上传
const formData = new FormData();
formData.append("personal_background", file);
fetch("/MyPage/upload-banner/", {
method: "POST",
headers: {
"X-CSRFToken": getCookie("csrftoken")
},
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
console.log("背景图上传成功");
} else {
console.error("上传失败:", data.message);
}
})
.catch(error => {
console.error("请求错误:", error);
});
}
}
// 页面背景
function changePageBg(event) {
const input = event.target;
const file = input.files[0];
if (file) {
// 本地预览
const reader = new FileReader();
reader.onload = function (e) {
document.body.style.backgroundImage = `url('${e.target.result}')`;
};
reader.readAsDataURL(file);
// 实时上传
const formData = new FormData();
formData.append("page_background", file);
fetch("/MyPage/upload-pagebg/", {
method: "POST",
headers: {
"X-CSRFToken": getCookie("csrftoken")
},
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
console.log("页面背景上传成功");
} else {
console.error("上传失败:", data.message);
}
})
.catch(error => {
console.error("请求错误:", error);
});
}
}
// 打开弹窗
function openProfileEdit() {
document.getElementById("profileModal").style.display = "block";
document.getElementById("modalOverlay").style.display = "block";
}
// 关闭弹窗
function closeProfileEdit() {
document.getElementById("profileModal").style.display = "none";
document.getElementById("modalOverlay").style.display = "none";
}
// 显示更新成功通知
function showProfileUpdateNotice() {
const notice = document.getElementById("profileUpdateNotice");
notice.style.display = "block";
// 3秒后自动消失
setTimeout(() => {
notice.style.display = "none";
}, 3000);
}
// 中栏
function openTab(evt, tabName) {
const tabcontent = document.getElementsByClassName("tabcontent");
for (let i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
const tablinks = document.getElementsByClassName("tablinks");
for (let i = 0; i < tablinks.length; i++) {
tablinks[i].classList.remove("active");
}
document.getElementById(tabName).style.display = "block";
evt.currentTarget.classList.add("active");
}
document.addEventListener("DOMContentLoaded", () => {
document.getElementById("defaultOpen").click();
});
//删除帖子
function toggleOptionsMenuDelete(button) {
const container = button.parentElement;
const menu = container.querySelector('.options-menu');
// 关闭其他菜单
document.querySelectorAll('.options-menu').forEach(m => {
if (m !== menu) m.style.display = 'none';
});
// 切换当前菜单
if (menu.style.display === 'block') {
menu.style.display = 'none';
} else {
menu.style.display = 'block';
}
}
// 点击空白处自动关闭
document.addEventListener('click', function (event) {
if (!event.target.closest('.options-menu') && !event.target.closest('button')) {
document.querySelectorAll('.options-menu').forEach(menu => {
menu.style.display = 'none';
});
}
});
// //自定义背景
// document.getElementById("bgInput").addEventListener("change", function () {
// let file = this.files[0];
// if (file) {
// let reader = new FileReader();
// reader.onload = function (e) {
// document.body.style.backgroundImage = `url('${e.target.result}')`;
// };
// reader.readAsDataURL(file);
// }
// });
// // 编辑个人资料
// // 打开弹窗
// function openProfileEdit() {
// document.getElementById("profileModal").style.display = "block";
// document.getElementById("modalOverlay").style.display = "block";
// // 预填充信息
// document.getElementById("editName").value = document.querySelector(".profile-name").textContent;
// document.getElementById("editLocation").value = document.querySelector(".profile-location").textContent.replace("📍 ", "");
// document.getElementById("editBio").value = document.querySelector(".profile-bio").textContent;
// }
// // 关闭弹窗
// function closeProfileEdit() {
// document.getElementById("profileModal").style.display = "none";
// document.getElementById("modalOverlay").style.display = "none";
// }
// // 更新资料
// document.getElementById("saveProfile").addEventListener("click", function () {
// const newName = document.getElementById("editName").value.trim();
// const newLocation = document.getElementById("editLocation").value.trim();
// const newBio = document.getElementById("editBio").value.trim();
// if (newName) document.querySelector(".profile-name").textContent = newName;
// if (newLocation) document.querySelector(".profile-location").innerHTML = `<i class="fas fa-map-marker-alt"></i> ${newLocation}`;
// if (newBio) document.querySelector(".profile-bio").textContent = newBio;
// // 关闭弹窗
// closeProfileEdit();
// // 显示更新成功通知
// showProfileUpdateNotice();
// });
// // 显示更新成功通知
// function showProfileUpdateNotice() {
// const notice = document.getElementById("profileUpdateNotice");
// notice.style.display = "block";
// // 3秒后自动消失
// setTimeout(() => {
// notice.style.display = "none";
// }, 3000);
// }
// // 更换个人资料背景图
// function changeBanner(event) {
// const file = event.target.files[0];
// if (file) {
// const reader = new FileReader();
// reader.onload = function (e) {
// document.getElementById("profileBanner").style.backgroundImage = `url(${e.target.result})`;
// };
// reader.readAsDataURL(file);
// }
// }
// // 更换头像
// function changeAvatar(event) {
// const file = event.target.files[0];
// if (file) {
// const reader = new FileReader();
// reader.onload = function (e) {
// document.getElementById("profileAvatar").src = e.target.result;
// };
// reader.readAsDataURL(file);
// }
// }
// // 好友显示区
// // 生成 DiceBear 头像 URL
// function getAvatarUrl(id) {
// return `https://api.dicebear.com/7.x/pixel-art/svg?seed=${id}`;
// }
// // 生成好友 HTML 结构
// function createFriendElement(friend) {
// return `
// <div class="friend-item ${friend.status}">
// <img src="${getAvatarUrl(friend.id)}" alt="${friend.name} 头像" onclick="viewProfile('${friend.id}')">
// <span onclick="viewProfile('${friend.id}')">${friend.name}</span>
// <button class="message-btn" onclick="sendMessage('${friend.id}')">
// <i class="fas fa-comment-dots"></i>
// </button>
// </div>
// `;
// }
// // 按照姓名首字母排序
// function sortFriendsByName(friendsList) {
// return friendsList.sort((a, b) => a.name.localeCompare(b.name, 'zh'));
// }
// // 渲染好友列表(按默认排序)
// function renderFriends(searchTerm = "") {
// const friendListContainer = document.getElementById("friendList");
// const specialGroup = document.getElementById("special-group");
// const otherGroup = document.getElementById("other-group");
// // 清空原有内容
// friendListContainer.innerHTML = "";
// specialGroup.innerHTML = "";
// otherGroup.innerHTML = "";
// // 按姓名首字母排序
// const sortedFriends = sortFriendsByName(friends);
// sortedFriends.forEach(friend => {
// const friendHTML = createFriendElement(friend);
// if (searchTerm) {
// // 进行搜索
// if (friend.name.toLowerCase().includes(searchTerm.toLowerCase())) {
// friendListContainer.innerHTML += friendHTML;
// }
// } else {
// // 默认展示所有好友(按首字母排序)
// friendListContainer.innerHTML += friendHTML;
// }
// // 分组显示(保持不变)
// if (friend.group === "special") {
// specialGroup.innerHTML += friendHTML;
// } else if (friend.group === "other") {
// otherGroup.innerHTML += friendHTML;
// }
// });
// }
// // 搜索好友(只影响主列表)
// function searchFriends() {
// let input = document.getElementById('friendSearch').value.trim();
// renderFriends(input);
// }
// // 展开/收起好友分组
// function toggleGroup(groupId) {
// const group = document.getElementById(groupId);
// const icon = document.getElementById(groupId + '-icon');
// group.classList.toggle("show");
// icon.classList.toggle("fa-chevron-up");
// icon.classList.toggle("fa-chevron-down");
// }
// // 好友卡片操作
// function viewProfile(friendId) {
// alert("查看 " + friendId + " 的主页");
// window.location.href = "../手语教室/SLClassroom.html";
// }
// function sendMessage(friendId) {
// alert("与 " + friendId + " 发送私信");
// window.location.href = "../手语教室/SLClassroom.html";
// }
// // 页面加载时渲染好友列表
// document.addEventListener("DOMContentLoaded", () => renderFriends());
// // 默认展示n条帖子
// const postsPerPage = 5;
// // 🟢 **主函数:切换 Tab 并渲染帖子**
// function openTab(evt, tabName) {
// var i, tabcontent, tablinks;
// tabcontent = document.getElementsByClassName("tabcontent");
// for (i = 0; i < tabcontent.length; i++) {
// tabcontent[i].style.display = "none";
// }
// tablinks = document.getElementsByClassName("tablinks");
// for (i = 0; i < tablinks.length; i++) {
// tablinks[i].className = tablinks[i].className.replace(" active", "");
// }
// document.getElementById(tabName).style.display = "block";
// evt.currentTarget.className += " active";
// tabPages[tabName] = 1; // 切换 Tab 时重置到第一页
// renderPosts(tabName);
// }
// // 🟢 **渲染帖子列表**
// function renderPosts(tabName) {
// const postContainer = document.querySelector(`#${tabName} .postContainer`);
// postContainer.innerHTML = "";
// const postList = postsData[tabName] || [];
// const currentPage = tabPages[tabName];
// // 计算总页数
// const totalPages = Math.ceil(postList.length / postsPerPage) || 1;
// // 计算分页范围
// const start = (currentPage - 1) * postsPerPage;
// const end = start + postsPerPage;
// const postsToShow = postList.slice(start, end);
// // 渲染帖子
// postsToShow.forEach(post => {
// const postElement = document.createElement("div");
// postElement.className = "feed-post";
// postElement.innerHTML = `
// <div class="post-header">
// <div class="post-avatar">
// <img src="https://api.dicebear.com/7.x/pixel-art/svg?seed=${post.user}321" alt="像素风头像">
// </div>
// <div class="post-info">
// <p class="post-user-name">${post.user}</p>
// <p class="post-time">${post.time}</p>
// </div>
// <!-- 🔹 右上角扩展栏 -->
// <div class="post-options">
// <i class="fas fa-ellipsis-h" onclick="toggleOptionsMenu(this)"></i>
// <div class="options-menu">
// <button onclick="toggleFavorite('${post.id}')">
// <i class="fas fa-heart"></i> 收藏
// </button>
// <button onclick="gotoPage('${post.user}')">
// <i class="fas fa-flag"></i> 查看个人主页
// </button>
// </div>
// </div>
// </div>
// <div class="post-content">
// <p>${post.content}</p>
// </div>
// <!-- 🔹 帖子交互区 -->
// <div class="post-actions">
// <button class="like-btn" onclick="toggleLike('${post.id}')">
// <i class="fas fa-thumbs-up"></i> <span id="likeCount-${post.id}">${post.likes}</span>
// </button>
// <button class="comment-btn" onclick="openComments('${post.id}')">
// <i class="fas fa-comment-dots"></i> 评论
// </button>
// <button class="share-btn" onclick="sharePost('${post.id}')">
// <i class="fas fa-share"></i> 转发
// </button>
// </div>
// `;
// postContainer.appendChild(postElement);
// });
// // 更新分页信息
// document.querySelector(`#${tabName} .page-info`).textContent = `第 ${currentPage} 页 / 共 ${totalPages} 页`;
// // 更新分页按钮状态
// document.querySelector(`#${tabName} .prev-btn`).disabled = currentPage === 1;
// document.querySelector(`#${tabName} .next-btn`).disabled = currentPage === totalPages;
// }
// // 🟢 **分页功能**
// function changePage(tabName, offset) {
// const maxPage = Math.ceil(postsData[tabName].length / postsPerPage) || 1;
// const newPage = tabPages[tabName] + offset;
// if (newPage >= 1 && newPage <= maxPage) {
// tabPages[tabName] = newPage;
// renderPosts(tabName);
// }
// }
// function jumpToPage(tabName) {
// const input = document.querySelector(`#${tabName} .page-input`);
// const maxPage = Math.ceil(postsData[tabName].length / postsPerPage) || 1;
// const newPage = Math.max(1, Math.min(maxPage, parseInt(input.value, 10) || 1));
// if (newPage !== tabPages[tabName]) {
// tabPages[tabName] = newPage;
// renderPosts(tabName);
// }
// }
// // 🟢 **页面加载时,默认打开第一个 Tab**
// document.addEventListener("DOMContentLoaded", function () {
// document.getElementById("defaultOpen").click();
// });
// // 帖子交互
// // 1️⃣ 切换收藏
// function toggleFavorite(postId) {
// alert("已收藏帖子:" + postId);
// }
// // 2️⃣ 查看个人主页
// function gotoPage(postUser) {
// alert("进入" + postUser + "的主页");
// window.location.href = "./myPage.html";
// }
// // 3️⃣ 点赞帖子
// function toggleLike(postId) {
// let likeCountElement = document.getElementById(`likeCount-${postId}`);
// let currentLikes = parseInt(likeCountElement.innerText);
// likeCountElement.innerText = currentLikes + 1;
// }
// // 4️⃣ 展开/隐藏扩展菜单
// function toggleOptionsMenu(iconElement) {
// let menu = iconElement.nextElementSibling;
// menu.style.display = menu.style.display === "flex" ? "none" : "flex";
// }
// // 5️⃣ 打开评论框(示例)
// function openComments(postId) {
// alert("打开评论区:" + postId);
// }
// // 6️⃣ 转发帖子(示例)
// function sharePost(postId) {
// alert("分享帖子:" + postId);
// }
// 通知区
// 进入教室
// function gotoClassroom() {
// window.location.href = "../手语教室/SLClassroom.html";
// }
// // 其他互动
// document.addEventListener("DOMContentLoaded", function () {
// const checkinBtn = document.querySelector(".checkin-btn");
// let checkinDays = 3; // 假设已签到3天
// // 🏆 签到功能
// checkinBtn.addEventListener("click", function () {
// checkinBtn.innerText = "✅ 已签到";
// checkinBtn.disabled = true;
// checkinBtn.style.background = "#9E9E9E";
// checkinDays += 1;
// document.querySelector(".checkin-btn + p b").innerText = checkinDays;
// // 签到成功提示
// let successMsg = document.createElement("div");
// successMsg.innerText = "🎉 签到成功!";
// successMsg.style.position = "fixed";
// successMsg.style.top = "50px";
// successMsg.style.right = "20px";
// successMsg.style.background = "#6A1B9A";
// successMsg.style.color = "white";
// successMsg.style.padding = "10px 15px";
// successMsg.style.borderRadius = "5px";
// successMsg.style.opacity = "0";
// successMsg.style.transition = "opacity 0.5s ease";
// document.body.appendChild(successMsg);
// setTimeout(() => (successMsg.style.opacity = "1"), 100);
// setTimeout(() => {
// successMsg.style.opacity = "0";
// setTimeout(() => successMsg.remove(), 500);
// }, 1500);
// });
// // 🔔 互动通知
// document.querySelectorAll(".notifications li").forEach((item) => {
// item.addEventListener("click", function () {
// this.style.opacity = "0.6";
// });
// });
// // 📅 TODO 任务完成状态
// document.querySelectorAll(".todo-list li").forEach((task) => {
// task.addEventListener("click", function () {
// this.style.textDecoration = "line-through";
// this.style.opacity = "0.6";
// });
// });
// // 🔥 监听新互动消息(模拟)
// setTimeout(() => {
// let newNotif = document.createElement("li");
// newNotif.innerHTML =
// '<img src="https://api.dicebear.com/7.x/pixel-art/svg?seed=5"> <b>王五</b> 给你发了私信';
// newNotif.style.color = "#6A1B9A";
// newNotif.style.fontWeight = "bold";
// document.querySelector(".notifications ul").prepend(newNotif);
// }, 3000);
// });

+ 387
- 0
static/js/upload_tasks_evaluator.js View File

@ -0,0 +1,387 @@
import {
FilesetResolver,
HandLandmarker,
} from "/static/js/mediapipe/vision_bundle.mjs";
let handLandmarker;
let standardLandmarks = [];
let frameScores = [];
let matched = false;
let canvas, ctx;
let bestMatches = [];
let standardPlayTimer = null;
function normalizeLandmarks(landmarks) {
if (!landmarks || landmarks.length !== 21) return [];
const base = landmarks[0];
const normalized = landmarks.map(p => ({
x: p.x - base.x,
y: p.y - base.y
}));
const scale = Math.sqrt(
normalized.reduce((sum, p) => sum + p.x * p.x + p.y * p.y, 0) / normalized.length
);
return scale > 0 ? normalized.map(p => ({ x: p.x / scale, y: p.y / scale })) : normalized;
}
function drawCenteredLandmarks(ctx, landmarks, width, height) {
ctx.clearRect(0, 0, width, height);
const norm = normalizeLandmarks(landmarks);
const xs = norm.map(p => p.x);
const ys = norm.map(p => p.y);
const minX = Math.min(...xs), maxX = Math.max(...xs);
const minY = Math.min(...ys), maxY = Math.max(...ys);
const scale = Math.min(width, height) / Math.max(maxX - minX, maxY - minY) * 0.8;
const offsetX = width / 2 - (minX + maxX) / 2 * scale;
const offsetY = height / 2 - (minY + maxY) / 2 * scale;
const mapped = norm.map(p => ({ x: p.x * scale + offsetX, y: p.y * scale + offsetY }));
const connections = [
[0, 1], [1, 2], [2, 3], [3, 4],
[0, 5], [5, 6], [6, 7], [7, 8],
[0, 9], [9, 10], [10, 11], [11, 12],
[0, 13], [13, 14], [14, 15], [15, 16],
[0, 17], [17, 18], [18, 19], [19, 20],
];
ctx.strokeStyle = "#4f46e5";
ctx.lineWidth = 2;
connections.forEach(([start, end]) => {
ctx.beginPath();
ctx.moveTo(mapped[start].x, mapped[start].y);
ctx.lineTo(mapped[end].x, mapped[end].y);
ctx.stroke();
});
mapped.forEach(p => {
ctx.beginPath();
ctx.arc(p.x, p.y, 3, 0, 2 * Math.PI);
ctx.fillStyle = "#1d4ed8";
ctx.fill();
});
}
async function playStandardFrames(frames) {
const canvas = document.getElementById("standardCanvas");
const ctx = canvas.getContext("2d");
if (standardPlayTimer) clearInterval(standardPlayTimer);
let frameIndex = 0;
standardPlayTimer = setInterval(() => {
if (!frames[frameIndex]) return;
drawCenteredLandmarks(ctx, frames[frameIndex], canvas.width, canvas.height);
frameIndex++;
if (frameIndex >= frames.length) clearInterval(standardPlayTimer);
}, 1000);
}
document.getElementById("replayBtn").addEventListener("click", () => {
playStandardFrames(standardLandmarks);
});
document.getElementById("gestureSelect").addEventListener("change", async function () {
const word = this.value;
const res = await fetch(`/static/data/${word}.json`);
standardLandmarks = await res.json();
const video = document.getElementById("standardVideo");
video.src = `/static/videos/${word}.mp4`;
playStandardFrames(standardLandmarks);
});
function computeScore(user, standard) {
if (!user || !standard || user.length !== 21 || standard.length !== 21) return 1.0;
const normUser = normalizeLandmarks(user);
const normStandard = normalizeLandmarks(standard);
let total = 0, validCount = 0;
for (let i = 0; i < 21; i++) {
const u = normUser[i], s = normStandard[i];
if (!u || !s) continue;
const dx = u.x - s.x, dy = u.y - s.y;
total += Math.sqrt(dx * dx + dy * dy);
validCount++;
}
return validCount > 0 ? total / validCount : 1.0;
}
function updateFeedback(score, final = false, matchedFrames = []) {
const feedback = document.getElementById("feedback");
const avgMatchedScore = matchedFrames.length > 0
? matchedFrames.reduce((a, b) => a + (1 - b), 0) / matchedFrames.length
: (1 - score);
if (avgMatchedScore > 0.8) {
feedback.textContent = `🎯 匹配优秀(得分 ${avgMatchedScore.toFixed(2)}`;
feedback.className = "text-green-700 bg-green-100 ...";
} else if (avgMatchedScore > 0.6) {
feedback.textContent = `🙂 大致相似(得分 ${avgMatchedScore.toFixed(2)}`;
feedback.className = "text-yellow-700 bg-yellow-100 ...";
} else {
feedback.textContent = final
? `⚠️ 动作不匹配(平均得分 ${avgMatchedScore.toFixed(2)}`
: `继续分析中...`;
feedback.className = "text-gray-700 bg-gray-100 ...";
}
}
function drawAbsoluteLandmarks(ctx, landmarks, width, height) {
if (!landmarks) return;
const connections = [
[0, 1], [1, 2], [2, 3], [3, 4],
[0, 5], [5, 6], [6, 7], [7, 8],
[0, 9], [9, 10], [10, 11], [11, 12],
[0, 13], [13, 14], [14, 15], [15, 16],
[0, 17], [17, 18], [18, 19], [19, 20],
];
const points = landmarks.map(p => ({ x: p.x * width, y: p.y * height }));
ctx.strokeStyle = "#f43f5e";
ctx.lineWidth = 2;
connections.forEach(([s, e]) => {
ctx.beginPath();
ctx.moveTo(points[s].x, points[s].y);
ctx.lineTo(points[e].x, points[e].y);
ctx.stroke();
});
ctx.fillStyle = "#f43f5e";
points.forEach(p => {
ctx.beginPath();
ctx.arc(p.x, p.y, 4, 0, 2 * Math.PI);
ctx.fill();
});
}
function displayBestMatches(matches) {
const container = document.getElementById("topFramesContainer");
container.innerHTML = "";
if (!matches.length) return;
matches.forEach(({ imageData, landmarks }) => {
const card = document.createElement("canvas");
card.width = 320;
card.height = 240;
card.className = "rounded shadow";
const context = card.getContext("2d");
const img = new Image();
img.onload = () => {
context.drawImage(img, 0, 0, 320, 240);
drawAbsoluteLandmarks(context, landmarks, 320, 240);
};
img.src = imageData;
container.appendChild(card);
});
}
async function analyzeVideo(video) {
const interval = 200;
const duration = video.duration * 1000;
const frameResults = [];
for (let t = 0; t < duration; t += interval) {
video.currentTime = t / 1000;
await new Promise((res) => (video.onseeked = res));
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
const bitmap = await createImageBitmap(canvas);
const result = await handLandmarker.detect(bitmap);
if (result.landmarks.length > 0) {
frameResults.push({
time: t,
landmarks: result.landmarks[0],
imageData: canvas.toDataURL(),
});
}
}
frameScores = frameResults.map(() => 1.0);
const topFrames = [];
const matchedFrameScores = [];
standardLandmarks.forEach((standard, idx) => {
let best = { score: Infinity, index: -1 };
frameResults.forEach((frame, i) => {
const score = computeScore(frame.landmarks, standard);
if (score < best.score) {
best = { score, index: i };
}
frameScores[i] = Math.min(frameScores[i], score);
});
if (best.index >= 0) {
topFrames.push(frameResults[best.index]);
matchedFrameScores.push(best.score);
}
});
const matchRatio = frameScores.filter(s => 1 - s >= 0.75).length / frameScores.length;
const showMatch = matchRatio > 0;
const container = document.getElementById("topFramesContainer");
container.style.display = showMatch ? "flex" : "none";
if (!showMatch) {
document.getElementById("feedback").textContent = "❌ 未识别出有效手势,请重新尝试。";
container.innerHTML = "";
return;
}
updateFeedback(0, true, matchedFrameScores);
displayBestMatches(topFrames);
}
document.getElementById("videoUpload").addEventListener("change", function () {
const file = this.files[0];
if (!file) return;
const video = document.getElementById("uploadedVideo");
video.src = URL.createObjectURL(file);
video.onloadedmetadata = async () => {
matched = false;
frameScores = [];
canvas = document.getElementById("hiddenCanvas");
ctx = canvas.getContext("2d");
console.log("📽️ 视频元信息已加载,开始处理");
await analyzeVideo(video);
};
});
async function setup() {
const vision = await FilesetResolver.forVisionTasks(
'/static/js/mediapipe/wasm'
);
handLandmarker = await HandLandmarker.createFromOptions(vision, {
baseOptions: {
modelAssetPath: "/static/models/hand_landmarker.task",
},
runningMode: "IMAGE",
numHands: 1,
});
const initWord = document.getElementById("gestureSelect").value;
const res = await fetch(`/static/data/${initWord}.json`);
standardLandmarks = await res.json();
const standardVideo = document.getElementById("standardVideo");
if (standardVideo) {
standardVideo.src = `/static/videos/${initWord}.mp4`;
standardVideo.style.maxWidth = "100%";
standardVideo.style.height = "auto";
}
playStandardFrames(standardLandmarks);
console.log("✅ 模型和标准姿势加载完毕");
}
setup();
// 选择框
const cardVideo = document.getElementById("card-video");
const cardLive = document.getElementById("card-live");
const uploadSection = document.getElementById("uploadSection");
const liveSection = document.getElementById("liveSection");
// 初始状态
setActiveMode("video");
// 切换处理
cardVideo.addEventListener("click", () => setActiveMode("video"));
cardLive.addEventListener("click", () => {
setActiveMode("live");
startLiveCamera(); // ⬅️ 显式调用摄像头
});
function setActiveMode(mode) {
if (mode === "video") {
uploadSection.style.display = "block";
liveSection.style.display = "none";
cardVideo.classList.add("ring-2", "ring-blue-500", "bg-blue-50");
cardLive.classList.remove("ring-2", "ring-blue-500", "bg-blue-50");
} else {
uploadSection.style.display = "none";
liveSection.style.display = "block";
cardLive.classList.add("ring-2", "ring-blue-500", "bg-blue-50");
cardVideo.classList.remove("ring-2", "ring-blue-500", "bg-blue-50");
}
}
let liveStream = null;
let liveTimer = null;
const liveCanvas = document.createElement("canvas");
const liveCtx = liveCanvas.getContext("2d");
async function startLiveCamera() {
const video = document.getElementById("liveVideo");
video.style.transform = "scaleX(-1)";
try {
liveStream = await navigator.mediaDevices.getUserMedia({ video: true });
video.srcObject = liveStream;
video.onloadedmetadata = () => {
video.play();
liveCanvas.width = video.videoWidth;
liveCanvas.height = video.videoHeight;
};
document.getElementById("startLiveBtn").classList.add("hidden");
document.getElementById("stopLiveBtn").classList.remove("hidden");
liveLoop();
} catch (err) {
alert("🚫 无法访问摄像头: " + err.message);
}
}
function stopLiveCamera() {
if (liveStream) {
liveStream.getTracks().forEach(track => track.stop());
liveStream = null;
}
cancelAnimationFrame(liveTimer);
document.getElementById("startLiveBtn").classList.remove("hidden");
document.getElementById("stopLiveBtn").classList.add("hidden");
document.getElementById("liveFeedback").textContent = "已停止实时检测。";
}
function mirrorLandmarksX(landmarks) {
return landmarks.map(p => ({
x: 1 - p.x,
y: p.y
}));
}
async function liveLoop() {
const video = document.getElementById("liveVideo");
liveCtx.drawImage(video, 0, 0, liveCanvas.width, liveCanvas.height);
const bitmap = await createImageBitmap(liveCanvas);
const result = await handLandmarker.detect(bitmap);
if (result.landmarks.length > 0) {
const mirroredLandmarks = mirrorLandmarksX(result.landmarks[0]);
let bestScore = Infinity;
for (const standard of standardLandmarks) {
const score = computeScore(mirroredLandmarks, standard);
bestScore = Math.min(bestScore, score);
}
const finalScore = 1 - bestScore;
const label = finalScore > 0.8 ? "🎯 匹配优秀"
: finalScore > 0.6 ? "🙂 大致相似"
: "⚠️ 动作不匹配";
document.getElementById("liveFeedback").textContent = `${label}(得分 ${finalScore.toFixed(2)}`;
} else {
document.getElementById("liveFeedback").textContent = "未检测到手部。";
}
liveTimer = requestAnimationFrame(liveLoop);
}
// === 绑定按钮 === //
document.getElementById("startLiveBtn").addEventListener("click", startLiveCamera);
document.getElementById("stopLiveBtn").addEventListener("click", stopLiveCamera);

BIN
static/models/hand_landmarker.task View File


BIN
static/models/vision_wasm_internal.wasm View File


BIN
static/models/vision_wasm_nosimd_internal.wasm View File


BIN
static/videos/你好.mp4 View File


BIN
static/videos/早上好.mp4 View File


BIN
static/videos/谢谢.mp4 View File


+ 474
- 0
templates/Community.html View File

@ -0,0 +1,474 @@
{% load static %}
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>手语社区 - 动态</title>
<link rel="stylesheet" href="{% static 'css/Community.css' %}">
<link rel="stylesheet" href="{% static 'css/login_button.css' %}">
<!-- 最新免费版 Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script src=https://cdn.tailwindcss.com></script>
<script>
tailwind.config = {
darkMode: `class`
}
</script>
</head>
<body>
<header class="py-6 px-4 md:px-8 flex justify-between items-center">
<div class="absolute left-1/2 transform -translate-x-1/2">
<a class="text-3xl md:text-4xl font-bold fade-in">WaveSign手语通</a>
<p class="text-lg md:text-xl opacity-80 fade-in mt-2">连接双手与世界,构建无声的桥梁
</p>
</div>
<div class="flex items-center space-x-4 ml-auto">
{% if user.is_authenticated %}
<div class="user-menu-box-container">
<a href="{% url 'my_page' %}" class="user-menu-box-button">
{% if user.avatar %}
<img src="{{ user.avatar.url }}" alt="头像" class="user-menu-box-avatar">
{% else %}
<img src="{% static 'images/default_avatar.png' %}" alt="默认头像" class="user-menu-box-avatar">
{% endif %}
{{ user.username }}
</a>
<div class="user-menu-box-dropdown">
<a href="{% url 'my_page' %}">进入个人主页</a>
<a href="{% url 'logout' %}">退出登录</a>
</div>
</div>
{% else %}
<a href="{% url 'login' %}" class="user-menu-box-login">
<i class="fas fa-user"></i> 登录
</a>
{% endif %}
</div>
</header>
<nav class="sticky top-0 bg-opacity-90 backdrop-blur-sm py-4 px-4 md:px-8 z-50 fade-in"
style="background-color:var(--card-bg)">
<div class="container mx-auto flex justify-center space-x-8">
<a class="nav-link text-lg font-medium" href="{% url 'home' %}" style="color:var(--text-color)">首页</a>
<a class="nav-link text-lg font-medium" href="{% url 'sl_classroom' %}"
style="color:var(--text-color)">手语教室</a>
<a class="nav-link text-lg font-medium" href="{% url 'life_serving' %}"
style="color:var(--text-color)">生活服务</a>
<a class="nav-link text-lg font-medium" href="{% url 'community' %}"
style="color:var(--text-color)">手语社区</a>
<a class="nav-link text-lg font-medium" href="{% url 'schedule' %}" style="color:var(--text-color)">日程管理</a>
<a class="nav-link text-lg font-medium" href="{% url 'my_page' %}" style="color:var(--text-color)">我的</a>
</div>
</nav>
<nav class="navbar">
<div class="logo">
<span class="title-highlight"><i class="fa-regular fa-hand-spock"></i>手语社区
</div></span>
<div class="search-box">
<input type="text" class="search-input" placeholder="搜索动态或用户...">
<button class="search-button">🔍</button>
</div>
</nav>
<h1 class="section-title"></h1>
<div class="container">
<div class="post-list">
<!-- 发布框 -->
<form method="post" action="/Community/create_post/" enctype="multipart/form-data">
{% csrf_token %}
<div class="post-editor">
<textarea class="editor-input" placeholder="分享你的想法或手语视频..." name="content"></textarea>
<div class="post-actions">
<div class="action-buttons">
<button type="button" onclick="document.getElementById('image').click()" title="添加图片">
📸 图片
</button>
<input type="file" id="image" name="image" accept="image/*" hidden>
<button type="button" onclick="document.getElementById('video').click()" title="添加视频">
🎥 视频
</button>
<input type="file" id="video" name="video" accept="video/*" hidden>
<button type="button" onclick="addTag()" title="添加标签">
🏷️ 标签
</button>
<input type="text" id="tags" name="tags" placeholder="#标签" hidden>
</div>
<button type="submit" class="post-button">发布</button>
</div>
</div>
</form>
<!-- 动态示例 -->
<!-- <div class="post-card">
<div class="post-header">
<img src="{% static 'images/proflie1.png'%}" class="user-avatar" alt="用户头像">
<div>
<h3>手语达人小王</h3>
<p>2小时前 · 📍北京</p>
</div>
</div>
<div class="post-content">
<p>今天的手语教学视频来啦!教大家"谢谢"和"不客气"的表达方式 👇</p>
</div>
<div class="post-media">
<video controls style="max-width: 50%; height: auto;">
<source src="{% static 'images/community_video.mp4'%}" type="video/mp4">
</video>
</div>
<div class="post-tags">
<span class="tag">#手语教学</span>
<span class="tag">#每日一课</span>
</div>
<div class="post-actions">
<div class="action-button">❤️ 1.2万</div>
<div class="action-button">💬 845</div>
<div class="action-button">🔄 326</div>
</div>
</div>
<div class="post-card">
<div class="post-header">
<img src="{% static 'images/profile2.png'%}" class="user-avatar" alt="用户头像">
<div>
<h3>努力生长的小苗</h3>
<p>1天前 · 📍上海</p>
</div>
</div>
<div class="post-content">
<p>好开心!今天收到社区的通知,发布了好多适合咱们聋哑人的岗位,感受到了满满爱意~</p>
</div>
<div class="post-tags">
<span class="tag">#工作岗位</span>
<span class="tag">#心情分享</span>
</div>
<div class="post-actions">
<div class="action-button">❤️ 3208</div>
<div class="action-button">💬 45</div>
<div class="action-button">🔄 285</div>
</div>
</div>
<div class="post-card">
<div class="post-header">
<img src="{% static 'images/profile3.png'%}" class="user-avatar" alt="用户头像">
<div>
<h3>超超家的花</h3>
<p>1天前 · 📍云南</p>
</div>
</div>
<div class="post-content">
<p>给猫猫草按时浇水的第9天,已经长出3cm高了!感谢好天气</p>
</div>
<div class="post-tags">
<span class="tag">#种植</span>
<span class="tag">#心情分享</span>
</div>
<div class="post-actions">
<div class="action-button">❤️ 227</div>
<div class="action-button">💬 85</div>
<div class="action-button">🔄 32</div>
</div>
</div> -->
{% for post in posts %}
<div class="post-card">
<!-- 用户信息区 -->
<div class="post-header relative">
{% if post.user.avatar %}
<img class="w-10 h-10 rounded-full object-cover" src="{{ post.user.avatar.url }}"
class="user-avatar" alt="用户头像">
{% else %}
<img src="{% static 'images/default_avatar.png' %}" class="user-avatar" alt="默认头像">
{% endif %}
<div>
<h3>{{ post.user.username }}</h3>
<p>{{ post.created_at|timesince }}前</p>
</div>
<!-- 关注按钮 -->
{% if request.user != post.user %}
<button
class="absolute top-2 right-2 bg-blue-500 text-white text-sm px-3 py-1 rounded hover:bg-blue-600 follow-btn action-button"
data-user-id="{{ post.user.id }}">
<i class="fas {% if post.is_followed %}fa-user-check{% else %}fa-user-plus{% endif %}"></i>
<span class="follow-text">
{% if post.is_followed %}已关注{% else %}关注{% endif %}
</span>
</button>
{% endif %}
</div>
<!-- 帖子内容 -->
<div class="post-content">
<p>{{ post.content }}</p>
</div>
<!-- 媒体文件(视频/图片) -->
<div class="post-media">
{% if post.image %}
<img src="{{ post.image.url }}" style="max-width: 100%;" alt="帖子图片">
{% endif %}
{% if post.video %}
<video controls style="max-width: 50%; height: auto;">
<source src="{{ post.video.url }}" type="video/mp4">
</video>
{% endif %}
</div>
<!-- 互动按钮 -->
<div class="post-actions">
<!-- ❤️ 点赞 -->
<button class="action-button like-btn" data-post-id="{{ post.id }}">
<i class="fa-regular fa-heart"></i> {{ post.like_count }}
</button>
<!-- 💬 评论 -->
<div class="flex justify-between items-center mb-2">
<div class="text-sm text-gray-600">
<i class="fa-regular fa-comment-dots"></i> {{ post.comment_count }}
</div>
</div>
<!-- ⭐ 收藏 -->
<button class="action-button favorite-btn" data-post-id="{{ post.id }}">
<i class="fa-regular fa-bookmark"></i> {{ post.favorite_count }}
</button>
</div>
<!-- 评论区 -->
<div class="post-comments">
<div class="comment-list" id="comment-list-{{ post.id }}">
{% for comment in post.comment_set.all %}
<div class="comment-item">
<strong>{{ comment.user.username }}</strong>: {{ comment.content }}
</div>
{% endfor %}
</div>
<form class="comment-form flex items-center mt-2 gap-2" data-post-id="{{ post.id }}">
<input type="text" name="content"
class="flex-1 px-3 py-2 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-400"
placeholder="写下你的评论..." required>
<button type="submit"
class="px-4 py-2 bg-blue-500 text-white rounded-xl hover:bg-blue-600 transition">发送</button>
</form>
</div>
</div>
{% endfor %}
</div>
<!-- 侧边栏 -->
<div class="sidebar">
<div class="trending-topics">
<h3>热门话题</h3>
<div class="topic-item">#无障碍设施指南</div>
<div class="topic-item">#手语故事分享</div>
<div class="topic-item">#就业信息速递</div>
<div class="topic-item">#手语诗歌创作</div>
</div>
</div>
</div>
<hr class="dashed">
<footer class="py-8 px-4 md:px-8 text-center fade-in" style="animation-delay:1.2s">
<div class="mx-auto max-w-4xl"> <!-- 典型内容宽度 -->
<div class="flex flex-col items-center justify-center">
<div class="mb-4 text-lg font-medium">作者: backpack</div>
<p class="text-sm opacity-70">© 2025 WaveSign手语通.</p>
</div>
</div>
</footer>
<script>
const isLoggedIn = {{ user.is_authenticated|yesno:"true,false"|safe }};
// 获取 CSRF Token 辅助函数
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let cookie of cookies) {
cookie = cookie.trim();
if (cookie.startsWith(name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
// 点赞按钮
document.querySelectorAll('.like-btn').forEach(button => {
button.addEventListener('click', () => {
if (!isLoggedIn) {
alert("请先登录再点赞!");
window.location.href = "/users/login/";
return;
}
const postId = button.getAttribute('data-post-id');
fetch(`/Community/like_post/${postId}/`, {
method: 'POST',
headers: {
'X-CSRFToken': getCookie('csrftoken'),
'Content-Type': 'application/json'
},
credentials: 'same-origin',
})
.then(response => response.json())
.then(data => {
if (data.success) {
button.innerHTML = `❤️ ${data.likes}`;
button.disabled = true;
} else {
alert(data.message || '您已经点过赞了');
}
});
});
});
// 评论提交
document.querySelectorAll('.comment-form').forEach(form => {
form.addEventListener('submit', function (e) {
e.preventDefault();
if (!isLoggedIn) {
alert("请先登录后再发表评论!");
window.location.href = "/users/login/";
return;
}
const postId = form.getAttribute('data-post-id');
const input = form.querySelector('input[name="content"]');
const content = input.value.trim();
if (!content) return;
fetch(`/Community/comment_post/${postId}/`, {
method: 'POST',
headers: {
'X-CSRFToken': getCookie('csrftoken'),
'Content-Type': 'application/x-www-form-urlencoded'
},
body: `content=${encodeURIComponent(content)}`,
credentials: 'same-origin',
})
.then(res => res.json())
.then(data => {
if (data.success) {
const list = document.getElementById(`comment-list-${postId}`);
const newComment = document.createElement('div');
newComment.className = 'comment-item';
newComment.innerHTML = `<strong>${data.username}</strong>: ${data.content}`;
list.appendChild(newComment);
input.value = '';
} else {
alert(data.message || '评论失败');
}
});
});
});
// 收藏按钮
document.querySelectorAll('.favorite-btn').forEach(button => {
button.addEventListener('click', () => {
if (!isLoggedIn) {
alert("请先登录后再收藏!");
window.location.href = "/users/login/";
return;
}
const postId = button.getAttribute('data-post-id');
fetch(`/Community/favorite_post/${postId}/`, {
method: 'POST',
headers: {
'X-CSRFToken': getCookie('csrftoken'),
'Content-Type': 'application/json'
},
credentials: 'same-origin',
})
.then(response => response.json())
.then(data => {
if (data.success) {
button.innerHTML = `⭐ ${data.favorites}`;
button.disabled = true;
} else {
alert(data.message || '您已经收藏过了');
}
});
});
});
// 关注按钮
document.querySelectorAll('.follow-btn').forEach(button => {
button.addEventListener('click', () => {
if (!isLoggedIn) {
alert("请先登录后再关注用户!");
window.location.href = "/users/login/";
return;
}
const userId = button.getAttribute('data-user-id');
fetch(`/Community/toggle_follow/${userId}/`, {
method: 'POST',
headers: {
'X-CSRFToken': getCookie('csrftoken'),
'Content-Type': 'application/json'
},
credentials: 'same-origin',
})
.then(res => res.json())
.then(data => {
if (data.success) {
const icon = button.querySelector('i');
const label = button.querySelector('.follow-text');
if (data.following) {
icon.classList.remove('fa-user-plus');
icon.classList.add('fa-user-check');
label.textContent = '已关注';
} else {
icon.classList.remove('fa-user-check');
icon.classList.add('fa-user-plus');
label.textContent = '关注';
}
} else {
alert(data.message);
}
});
});
});
</script>
</body>
</html>

+ 692
- 0
templates/Home.html View File

@ -0,0 +1,692 @@
{% load static %}
<!doctype html>
<html lang=zh-CN>
<head>
<meta charset=UTF-8>
<title>WaveSign手语通</title>
<link rel="stylesheet" href="{% static 'css/login_button.css' %}">
<script src=https://cdn.tailwindcss.com></script>
<script>
tailwind.config = {
darkMode: `class`
}
</script>
<link href=https://cdn.bootcdn.net/ajax/libs/font-awesome/6.4.0/css/all.min.css rel=stylesheet>
<style>
/* 新增幻灯片样式 */
.slider-container {
position: relative;
overflow: hidden;
border-radius: 12px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
margin: 2rem 0;
}
.slider-track {
display: flex;
width: 300%;
transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
.slider-item {
flex: 0 0 33.33333%;
position: relative;
overflow: hidden;
}
.slider-item img {
width: 100%;
height: 400px;
object-fit: cover;
transform: scale(1);
transition: transform 10s linear;
}
.slider-item:hover img {
transform: scale(1.05);
}
.slider-caption {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 2rem;
background: linear-gradient(to top, rgba(0, 0, 0, 0.7), transparent);
color: white;
}
.slider-control {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 100%;
display: flex;
justify-content: space-between;
padding: 0 1rem;
}
.slider-nav {
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(5px);
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s;
}
.slider-nav:hover {
background: rgba(255, 255, 255, 0.3);
transform: scale(1.1);
}
.slider-dots {
position: absolute;
bottom: 1rem;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 8px;
}
.slider-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.5);
cursor: pointer;
transition: all 0.3s;
}
.slider-dot.active {
background: var(--primary);
width: 30px;
border-radius: 5px;
}
@media (max-width: 768px) {
.slider-item img {
height: 300px;
}
.slider-nav {
width: 35px;
height: 35px;
}
}
@import "https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&display=swap";
:root {
--primary: #8d6a95;
--primary-light: #cebbf3;
--dark-bg: #1a1a1a;
--dark-card: #2a2a2a;
--light-bg: #f9f9f9;
--light-card: #fff
}
@import "https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&display=swap";
:root {
--primary: #8d6a95;
--primary-light: #cebbf3;
--dark-bg: #1a1a1a;
--dark-card: #2a2a2a;
--light-bg: #f9f9f9;
--light-card: #fff
}
.dark {
--bg-color: var(--dark-bg);
--card-bg: var(--dark-card);
--text-color: #fff;
--border-color: #692479
}
.light {
--bg-color: var(--light-bg);
--card-bg: var(--light-card);
--text-color: hsl(301, 89%, 21%);
--border-color: #e5e5e5
}
body {
background-color: var(--bg-color);
color: var(--text-color);
font-family: Noto Sans SC, sans-serif;
transition: background-color .3s, color .3s;
padding: 2rem 5%;
/* 修改为百分比响应式边距 */
max-width: 1440px;
margin: 0 auto;
box-sizing: border-box;
}
/* 新增盒模型设置[10](@ref) */
@media (min-width: 1600px) {
body {
padding: 2rem 8%;
/* 大屏幕增加边距 */
}
}
@media (max-width: 768px) {
body {
padding: 1.5rem;
/* 移动端统一边距 */
}
}
/* 新增内容容器边距 */
.main-container {
padding: 0 2rem;
/* 内容区二级边距 */
margin: 0 auto;
}
.card {
background-color: var(--card-bg);
border: 1px solid var(--border-color);
transition: transform .3s, box-shadow .3s, border-color .3s;
margin: 1rem 0;
/* 新增卡片外边距 */
padding: 1.5rem;
/* 新增卡片内边距[2](@ref) */
}
.card:hover {
border-color: var(--primary);
transform: translateY(-5px);
box-shadow: 0 10px 20px #0000001a
}
.btn {
transition: transform .2s, background-color .2s
}
.btn:hover {
transform: scale(1.05)
}
.fade-in {
animation: .8s ease-in fadeIn
}
@keyframes fadeIn {
0% {
opacity: 0;
transform: translateY(20px)
}
to {
opacity: 1;
transform: translateY(0)
}
}
.nav-link {
position: relative
}
.nav-link:after {
content: "";
background-color: var(--primary);
width: 0;
height: 2px;
transition: width .3s;
position: absolute;
bottom: -2px;
left: 0
}
.nav-link:hover:after,
.nav-link.active:after {
width: 100%
}
.image-slide {
border-radius: 10px;
position: relative;
overflow: hidden
}
.image-slide img {
transition: transform 10s ease-in-out
}
.image-slide:hover img {
transform: scale(1.05)
}
.theme-toggle {
cursor: pointer;
border-radius: 50%;
padding: 10px;
transition: background-color .3s
}
.theme-toggle:hover {
background-color: var(--primary-light)
}
/* 添加在style中 */
.user-menu {
display: none;
position: absolute;
right: 0;
top: 100%;
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
min-width: 160px;
}
.user-menu.show {
display: block;
}
</style>
<script src="https://cdn.bootcdn.net/ajax/libs/font-awesome/6.4.0/js/all.min.js"></script>
</head>
<body>
<header class="py-6 px-4 md:px-8 flex justify-between items-center">
<div class="absolute left-1/2 transform -translate-x-1/2">
<a class="text-3xl md:text-4xl font-bold fade-in">WaveSign手语通</a>
<p class="text-lg md:text-xl opacity-80 fade-in mt-2">连接双手与世界,构建无声的桥梁
</p>
</div>
<div class="flex items-center space-x-4 ml-auto">
{% if user.is_authenticated %}
<div class="user-menu-box-container">
<a href="{% url 'my_page' %}" class="user-menu-box-button">
{% if user.avatar %}
<img src="{{ user.avatar.url }}" alt="头像" class="user-menu-box-avatar">
{% else %}
<img src="{% static 'images/default_avatar.png' %}" alt="默认头像" class="user-menu-box-avatar">
{% endif %}
{{ user.username }}
</a>
<div class="user-menu-box-dropdown">
<a href="{% url 'my_page' %}">进入个人主页</a>
<a href="{% url 'logout' %}">退出登录</a>
</div>
</div>
{% else %}
<a href="{% url 'login' %}" class="user-menu-box-login">
<i class="fas fa-user"></i> 登录
</a>
{% endif %}
</div>
</header>
<nav class="sticky top-0 bg-opacity-90 backdrop-blur-sm py-4 px-4 md:px-8 z-50 fade-in"
style="background-color:var(--card-bg); border-bottom: 1px solid var(--border-color);">
<div class="container mx-auto flex justify-center space-x-8">
<a class="nav-link text-lg font-medium" href="{% url 'home' %}" style="color:var(--text-color)">首页</a>
<a class="nav-link text-lg font-medium" href="{% url 'sl_classroom' %}"
style="color:var(--text-color)">手语教室</a>
<a class="nav-link text-lg font-medium" href="{% url 'life_serving' %}"
style="color:var(--text-color)">生活服务</a>
<a class="nav-link text-lg font-medium" href="{% url 'community' %}"
style="color:var(--text-color)">手语社区</a>
<a class="nav-link text-lg font-medium" href="{% url 'schedule' %}" style="color:var(--text-color)">日程管理</a>
<a class="nav-link text-lg font-medium" href="{% url 'my_page' %}" style="color:var(--text-color)">我的</a>
</div>
</nav>
<!-- 修改后的幻灯片部分 -->
<section class="mb-12 fade-in" style="animation-delay:.2s">
<div class="slider-container">
<div class="slider-track" id="sliderTrack">
<!-- 幻灯片项1 -->
<div class="slider-item">
<img src="{% static 'images/1.png' %}" alt="手语交流">
<div class="slider-caption">
<h3 class="text-2xl font-bold mb-2">手语交流社区</h3>
<p>与全球手语使用者实时互动</p>
</div>
</div>
<!-- 幻灯片项2 -->
<div class="slider-item">
<img src="{% static 'images/2.jpg' %}" alt="手语教学">
<div class="slider-caption">
<h3 class="text-2xl font-bold mb-2">专业课程体系</h3>
<p>系统化学习手语知识</p>
</div>
</div>
<!-- 幻灯片项3 -->
<div class="slider-item">
<img src="{% static 'images/3.jpg' %}">
<div class="slider-caption">
<h3 class="text-2xl font-bold mb-2">生活服务支持</h3>
<p>一站式解决日常需求</p>
</div>
</div>
</div>
<!-- 导航控件 -->
<div class="slider-control">
<div class="slider-nav" onclick="prevSlide()">
<i class="fas fa-chevron-left text-white"></i>
</div>
<div class="slider-nav" onclick="nextSlide()">
<i class="fas fa-chevron-right text-white"></i>
</div>
</div>
<!-- 指示器 -->
<div class="slider-dots" id="sliderDots"></div>
</div>
</section>
<section class="mb-16 fade-in" id=hand-sign style=animation-delay:.4s>
<h2 class="text-2xl md:text-3xl font-bold mb-6 flex items-center">
<i class="fas fa-sign-language mr-3 text-purple-600"></i>
手语教室
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div class="card rounded-xl p-6 hover:shadow-lg">
<div class="flex items-center mb-4">
<div
class="w-12 h-12 rounded-full bg-purple-100 dark:bg-purple-900 flex items-center justify-center mr-4">
<i class="fas fa-book text-purple-600 dark:text-purple-400 text-xl"></i>
</div>
<h3 class="text-xl font-semibold">基础手语课程</h3>
</div>
<p class=mb-4>从零开始学习手语,掌握日常交流必备词汇和语法。
<div class="flex justify-end">
<a class="btn text-purple-600 dark:text-purple-400 hover:text-purple-800 dark:hover:text-purple-300 font-medium"
href="{% url 'sl_classroom' %}" style="color:var(--text-color)">开始学习</a>
</div>
</div>
<div class="card rounded-xl p-6 hover:shadow-lg">
<div class="flex items-center mb-4">
<div
class="w-12 h-12 rounded-full bg-purple-100 dark:bg-purple-900 flex items-center justify-center mr-4">
<i class="fas fa-video text-purple-600 dark:text-purple-400 text-xl"></i>
</div>
<h3 class="text-xl font-semibold">视频教学</h3>
</div>
<p class=mb-4>通过生动的视频教程,直观学习手语表达方式。
<div class="flex justify-end">
<a class="btn text-purple-600 dark:text-purple-400 hover:text-purple-800 dark:hover:text-purple-300 font-medium"
href="{% url 'sl_classroom' %}" style="color:var(--text-color)">观看视频</a>
</div>
</div>
<div class="card rounded-xl p-6 hover:shadow-lg">
<div class="flex items-center mb-4">
<div
class="w-12 h-12 rounded-full bg-purple-100 dark:bg-purple-900 flex items-center justify-center mr-4">
<i class="fas fa-comments text-purple-600 dark:text-purple-400 text-xl"></i>
</div>
<h3 class="text-xl font-semibold">互动练习</h3>
</div>
<p class=mb-4>通过互动练习巩固所学知识,提高手语应用能力。
<div class="flex justify-end">
<a class="btn text-purple-600 dark:text-purple-400 hover:text-purple-800 dark:hover:text-purple-300 font-medium"
href="{% url 'sl_classroom' %}" style="color:var(--text-color)">开始练习</a>
</div>
</div>
</div>
</section>
<section class="mb-16 fade-in" id=life-service style=animation-delay:.6s>
<h2 class="text-2xl md:text-3xl font-bold mb-6 flex items-center">
<i class="fas fa-life-ring mr-3 text-purple-600"></i>
生活服务
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div class="card rounded-xl p-6 hover:shadow-lg">
<div
class="w-12 h-12 rounded-full bg-purple-100 dark:bg-purple-900 flex items-center justify-center mb-4">
<i class="fas fa-users text-purple-600 dark:text-purple-400 text-xl"></i>
</div>
<h3 class="text-xl font-semibold mb-2">社交活动</h3>
<p>发现附近的聋哑人士聚会和手语交流活动
</div>
<div class="card rounded-xl p-6 hover:shadow-lg">
<div
class="w-12 h-12 rounded-full bg-green-100 dark:bg-green-900 flex items-center justify-center mb-4">
<i class="fas fa-map-marker-alt text-green-600 dark:text-green-400 text-xl"></i>
</div>
<h3 class="text-xl font-semibold mb-2">无障碍设施</h3>
<p>获取无障碍设施的地点信息和路线指引。
</div>
<div class="card rounded-xl p-6 hover:shadow-lg">
<div
class="w-12 h-12 rounded-full bg-orange-100 dark:bg-orange-900 flex items-center justify-center mb-4">
<i class="fas fa-gift text-orange-600 dark:text-orange-400 text-xl"></i>
</div>
<h3 class="text-xl font-semibold mb-2">好物推荐</h3>
<p>精选适合聋哑人士的生活辅助工具和实用商品
</div>
<div class="card rounded-xl p-6 hover:shadow-lg">
<div
class="w-12 h-12 rounded-full bg-yellow-100 dark:bg-yellow-900 flex items-center justify-center mb-4">
<i class="fas fa-shopping-cart text-yellow-600 dark:text-yellow-400 text-xl"></i>
</div>
<h3 class="text-xl font-semibold mb-2">就业信息</h3>
<p>获取聋哑人士最新岗位信息和在线找工作支持。
</div>
</div>
</section>
<section class="mb-16 fade-in" id=sign-community style=animation-delay:.8s>
<h2 class="text-2xl md:text-3xl font-bold mb-6 flex items-center">
<i class="fas fa-users mr-3 text-purple-600"></i>
手语社区
</h2>
<div class="card rounded-xl p-6 mb-6">
<div class="flex flex-col md:flex-row">
<div class="md:w-1/3 mb-4 md:mb-0 md:pr-6">
<h3 class="text-xl font-semibold mb-3">最新活动</h3>
<ul class=space-y-2>
<li class="flex items-center">
<i class="fas fa-calendar-check text-purple-600 dark:text-purple-400 mr-2"></i>
<span>手语交流会 - 下周日</span>
<li class="flex items-center">
<i class="fas fa-calendar-check text-purple-600 dark:text-purple-400 mr-2"></i>
<span>手语工作坊 - 本月末</span>
<li class="flex items-center">
<i class="fas fa-calendar-check text-purple-600 dark:text-purple-400 mr-2"></i>
<span>手语表演 - 下月</span>
</ul>
</div>
<div class="md:w-2/3 md:border-l md:pl-6 border-gray-200 dark:border-gray-700">
<h3 class="text-xl font-semibold mb-3">社区动态</h3>
<div class=space-y-4>
<div class="p-4 rounded-lg bg-gray-50 dark:bg-gray-800">
<p class=mb-2>
<span class=font-medium>用户A:</span>
刚学了几个新的手语词汇,感觉越来越自信了!
<p class="text-sm text-gray-600 dark:text-gray-400">— 前天
</div>
<div class="p-4 rounded-lg bg-gray-50 dark:bg-gray-800">
<p class=mb-2>
<span class=font-medium>用户B:</span>
明天的手语交流会见!期待和大家见面。
<p class="text-sm text-gray-600 dark:text-gray-400">— 昨天
</div>
</div>
</div>
</div>
</div>
<div class="card rounded-xl p-6">
<h3 class="text-xl font-semibold mb-4">加入我们</h3>
<p class=mb-4>成为WaveSign社区的一员,与其他手语学习者和使用者交流互动,分享经验,共同进步。
<div class="flex justify-center">
<a class="btn bg-purple-600 hover:bg-purple-700 text-white px-6 py-2 rounded-lg font-medium"
href="{% url 'community' %}"
style="color:var(--text-color)">立即加入</a>
</div>
</div>
</section>
<footer class="py-8 px-4 md:px-8 text-center fade-in" style=animation-delay:1.2s>
<div class="flex flex-col items-center">
<div class="mb-4 text-lg font-medium">作者: backpack</div>
<p class="text-sm opacity-70">© 2025 WaveSign.
</div>
</footer>
<script>
// 幻灯片控制逻辑
let currentSlide = 0;
const slideCount = 3;
const sliderTrack = document.getElementById('sliderTrack');
const sliderDots = document.getElementById('sliderDots');
let autoPlayTimer;
// 初始化指示器
for (let i = 0; i < slideCount; i++) {
const dot = document.createElement('div');
dot.className = `slider-dot ${i === 0 ? 'active' : ''}`;
dot.addEventListener('click', () => goToSlide(i));
sliderDots.appendChild(dot);
}
// 切换幻灯片
function goToSlide(index) {
currentSlide = (index + slideCount) % slideCount;
sliderTrack.style.transform = `translateX(-${currentSlide * 33.333}%)`;
updateDots();
}
// 更新指示器状态
function updateDots() {
document.querySelectorAll('.slider-dot').forEach((dot, index) => {
dot.classList.toggle('active', index === currentSlide);
});
}
// 下一张
function nextSlide() {
goToSlide(currentSlide + 1);
resetAutoPlay();
}
// 上一张
function prevSlide() {
goToSlide(currentSlide - 1);
resetAutoPlay();
}
// 自动播放控制
function startAutoPlay() {
autoPlayTimer = setInterval(nextSlide, 5000);
}
function resetAutoPlay() {
clearInterval(autoPlayTimer);
startAutoPlay();
}
// 悬停控制
document.querySelector('.slider-container').addEventListener('mouseenter', () => {
clearInterval(autoPlayTimer);
});
document.querySelector('.slider-container').addEventListener('mouseleave', startAutoPlay);
// 初始化自动播放
startAutoPlay();
const themeToggle = document.getElementById(`themeToggle`);
const body = document.body;
if (window.matchMedia && window.matchMedia(`(prefers-color-scheme: dark)`).matches) {
body.classList.remove(`light`);
body.classList.add(`dark`)
}
; themeToggle.addEventListener(`click`, () => {
let b = `light`
, a = `dark`;
if (body.classList.contains(a)) {
body.classList.remove(a);
body.classList.add(b)
} else {
body.classList.remove(b);
body.classList.add(a)
}
}
);
// 示例登录状态检测
const authState = {
isLoggedIn: false, // 实际应从cookie/localStorage获取
username: "测试用户"
}
const renderAuth = () => {
const authHTML = authState.isLoggedIn
? `<div class="flex items-center space-x-2">
<i class="fa-regular fa-user text-xl"></i>
<span>${authState.username}</span>
</div>`
: `<a href="{% url 'login' %}" class="flex items-center space-x-2 hover:text-[var(--primary)] transition-colors">
<i class="fa-regular fa-user text-xl"></i>
<span class="text-lg">登录</span>
</a>`;
document.querySelector('header').lastElementChild.innerHTML = authHTML;
}
// 初始化执行
renderAuth();
const navLinks = document.querySelectorAll(`.nav-link`);
const sections = document.querySelectorAll(`section[id]`);
window.addEventListener(`scroll`, () => {
let b = `active`;
let a = ``;
sections.forEach(b => {
const c = b.offsetTop;
const d = b.clientHeight;
if (pageYOffset >= c - 200) {
a = b.getAttribute(`id`)
}
}
);
navLinks.forEach(c => {
c.classList.remove(b);
if (c.getAttribute(`href`).substring(1) === a) {
c.classList.add(b)
}
}
)
}
);
document.addEventListener(`DOMContentLoaded`, () => {
const a = document.querySelectorAll(`.fade-in`);
const b = new IntersectionObserver(a => {
a.forEach(a => {
if (a.isIntersecting) {
a.target.style.opacity = 1;
a.target.style.transform = `translateY(0)`
}
}
)
}
, {
threshold: 0.1
});
a.forEach(a => {
a.style.opacity = 0;
a.style.transform = `translateY(20px)`;
a.style.transition = `opacity 0.5s ease, transform 0.5s ease`;
b.observe(a)
}
)
}
)
</script>
</body>
</html>

+ 468
- 0
templates/LifeServing.html View File

@ -0,0 +1,468 @@
{% load static %}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>公共服务</title>
<link rel="stylesheet" href="../static/css/LifeServing.css">
<link rel="stylesheet" href="{% static 'css/login_button.css' %}">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
<link href=https://cdn.bootcdn.net/ajax/libs/font-awesome/6.4.0/css/all.min.css rel=stylesheet>
<script src=https://cdn.tailwindcss.com></script>
<script>
tailwind.config = {
darkMode: `class`
}
</script>
<script type="text/javascript" src="http://api.map.baidu.com/api?v=1.3"></script>
</head>
<body>
<header class="py-6 px-4 md:px-8 flex justify-between items-center">
<div class="absolute left-1/2 transform -translate-x-1/2">
<a class="text-3xl md:text-4xl font-bold fade-in">WaveSign手语通</a>
<p class="text-lg md:text-xl opacity-80 fade-in mt-2">连接双手与世界,构建无声的桥梁</p>
</div>
<div class="flex items-center space-x-4 ml-auto">
{% if user.is_authenticated %}
<div class="user-menu-box-container">
<a href="{% url 'my_page' %}" class="user-menu-box-button">
{% if user.avatar %}
<img src="{{ user.avatar.url }}" alt="头像" class="user-menu-box-avatar">
{% else %}
<img src="{% static 'images/default_avatar.png' %}" alt="默认头像" class="user-menu-box-avatar">
{% endif %}
{{ user.username }}
</a>
<div class="user-menu-box-dropdown">
<a href="{% url 'my_page' %}">进入个人主页</a>
<a href="{% url 'logout' %}">退出登录</a>
</div>
</div>
{% else %}
<a href="{% url 'login' %}" class="user-menu-box-login">
<i class="fas fa-user"></i> 登录
</a>
{% endif %}
</div>
</header>
<nav class="sticky top-0 bg-opacity-90 backdrop-blur-sm py-4 px-4 md:px-8 z-50 fade-in"
style="background-color:var(--card-bg)">
<div class="container mx-auto flex justify-center space-x-8">
<a class="nav-link text-lg font-medium" href="{% url 'home' %}" style="color:var(--text-color)">首页</a>
<a class="nav-link text-lg font-medium" href="{% url 'sl_classroom' %}" style="color:var(--text-color)">手语教室</a>
<a class="nav-link text-lg font-medium" href="{% url 'life_serving' %}" style="color:var(--text-color)">生活服务</a>
<a class="nav-link text-lg font-medium" href="{% url 'community' %}" style="color:var(--text-color)">手语社区</a>
<a class="nav-link text-lg font-medium" href="{% url 'schedule' %}" style="color:var(--text-color)">日程管理</a>
<a class="nav-link text-lg font-medium" href="{% url 'my_page' %}" style="color:var(--text-color)">我的</a>
</div>
</nav>
<nav class="nav-container">
<ul class="nav-menu">
<li class="nav-item" onmouseover="showContent('travel-service')">🚗 出行服务</li>
<li class="nav-item" onmouseover="showContent('shopping-guide')">🛍️ 好物推荐</li>
<li class="nav-item" onmouseover="showContent('job-info')">💼 就业信息</li>
<li class="nav-item" onmouseover="showContent('activity')">❤️ 近期活动</li>
</ul>
</nav>
<!-- 出行服务模块 -->
<div id="travel-service" class="content-container">
<!-- 地图与实时信息并排 -->
<div class="traffic-wrapper">
<!-- 地图容器 -->
<div class="map-container" id="container">
<iframe width="100%" height="500" frameborder="0"
src="https://map.baidu.com/@12958171.72,4828364.17,18z?query=深圳市华利嘉电子市场2c086">
</iframe>
<div class="traffic-legend">
<div class="legend-item">
<span class="traffic-dot traffic-smooth"></span>
<span>畅通</span>
</div>
<div class="legend-item">
<span class="traffic-dot traffic-slow"></span>
<span>缓行</span>
</div>
<div class="legend-item">
<span class="traffic-dot traffic-congested"></span>
<span>拥堵</span>
</div>
</div>
</div>
<!-- 实时信息侧边栏 -->
<div class="traffic-sidebar">
<div class="traffic-alert">
<h3>🚨 实时预警</h3>
<ul class="alert-list">
<li>【18:00】地铁2号线人民广场站电梯维护</li>
<li>【17:30】南京西路路段临时管制</li>
</ul>
</div>
<div class="sign-language-tip">
<h3>✋ 手语服务</h3>
<p>周边可提供手语帮助的站点:</p>
<ul class="service-list">
<li>人民广场站 (1号口服务台)</li>
<li>静安寺站 (无障碍服务中心)</li>
</ul>
</div>
</div>
</div>
<!-- 无障碍出行推荐 -->
<div class="accessibility-recommend">
<div class="recommend-card">
<div class="icon">👐</div>
<h4>手语导航服务</h4>
<p>在线预约手语翻译陪同出行</p>
<a href="https://vip.aminer.cn/sign/">
<button class="purple-btn">立即预约</button>
</a>
</div>
<div class="recommend-card">
<div class="icon">🦻</div>
<h4>震动提醒路线</h4>
<p>开启手机震动导航模式</p>
<a href="https://map.baidu.com/@13529169,3653875,12z,50t">
<button class="purple-btn">开启导航</button></a>
</div>
<div class="recommend-card">
<div class="icon">📸</div>
<h4>AR实景指引</h4>
<p>摄像头识别无障碍设施位置</p>
<a href="https://map.baidu.com/@13529169,3653875,12z,50t">
<button class="purple-btn">启动AR</button></a>
</div>
</div>
</div>
<!-- 好物推荐模块 -->
<div id="shopping-guide" class="content-container">
<!-- 分类导航 -->
<div class="category-nav">
<button class="category-btn active" data-category="all">全部</button>
<button class="category-btn" data-category="daily">生活日用</button>
<button class="category-btn" data-category="tech">科技助残</button>
<button class="category-btn" data-category="safety">安全防护</button>
</div>
<!-- 商品展示区 -->
<div class="goods-grid">
<!-- 商品卡片1 -->
<div class="goods-card" data-category="tech">
<div class="goods-img" style="background-image: url('../static/images/item1.png')">
<button class="favorite-btn">❤️ 收藏</button>
</div>
<div class="goods-info">
<h3>智能震动闹钟</h3>
<p class="goods-desc">枕头震动唤醒,静音不扰人</p>
<div class="price-tag">
<span class="current-price">¥268</span>
<span class="original-price">¥399</span>
</div>
<a
href="https://item.taobao.com/item.htm?spm=a21n57.sem.item.9.5af23903hZxXlt&priceTId=2147801b17432965612948760e16f3&utparam=%7B%22aplus_abtest%22%3A%228b1b1e20617466057956ef371f7c333d%22%7D&id=750422232833&ns=1&xxc=ad_ztc&pisk=gAcmD-4Iyxyf5pK8ebNXpMznXQ9-kSN_xcCTX5EwUur5H5pjWPDoRcqv5KNxElmrqo5vlqnu7c0sHEw9cNbjZDfOMn9j71VT_HKp9B3fl5NwvvitPNSbSy526rPaaSaOkprrCB3jll_cbeHy9GADhvOVQloaa8zT-5z43lRyrP43b5r4QgWz8uPa_oz4UuzTotzabo8l4yUN_PyaQuyzlP14_5owaar775zqb5kGvKrj_bly4aYLQNnJRba0nku40q3sanyAhqEoCAczo4ctuO1N_b40nf9w-is8HAuQdj39s_Nx-YPzSfTFIl2nQjFrg3RUFRkZpRDR0EnEj4o_Mb8N3kkxD7lqKN5ZrSqZxbyFm3qEG4lQa8de-4lSDqGoWNRayDErljPMTeN0goPuPfK5glDrQjeb1i-gfb0rgAjr_TWFAqC_zFhPCOw4PzqpMbNBVGI2zF8kr98_3zabvUYlCOw4PzqprUXe1-z7lkC..">
<button class="buy-btn">立即购买</button>
</a>
</div>
</div>
<!-- 商品卡片2 -->
<div class="goods-card" data-category="safety">
<div class="goods-img" style="background-image: url('../static/images/item2.png')">
<button class="favorite-btn">❤️ 收藏</button>
</div>
<div class="goods-info">
<h3>闪光门铃</h3>
<p class="goods-desc">灯光+震动双重提醒</p>
<div class="price-tag">
<span class="current-price">¥158</span>
</div>
<a
href="https://detail.tmall.com/item.htm?spm=a21n57.sem.item.59.5af23903hZxXlt&priceTId=2147801b17432968509466789e16f3&utparam=%7B%22aplus_abtest%22%3A%22fa6c66f4cf0640a81bd9874522aa55e0%22%7D&id=814651206485&ns=1&xxc=ad_ztc&pisk=goiIcli0Z6fCVwqRFyvaCI4PFNEWNd-2dTw-nYIFekEd2LGr_0z8aec7PblgyDrELfN-gfNd4UVFVuMSZblSEp28wbh7a28w0vD3qudqNn-qKZcGf_lCvWe9e8eYpdzduqhjAud2g3_NByAU2XlV53YOW-VTp8ULpdZTE8jL2zeKBdw8U6QRvbp6C8ebeMUdJGUTn8qRwyFRWGezU7Q8wgI9C8VO9uE-wlp_EAL21RYQ9v9LMKPfga_jT5sR2cw9ZRMBsiwEVJG3gvnB1C9udyNxp5OmjZazWY4SqTj7zv3mZ-hd9QNofAnKkjdN8W3bFxuSML73Qzi3f7npmtNtAYi_-V5dJ-ZQOrnLk39jHz3sfkopZZDZCWUbbV8MTzrIOqV0JFAiNANEwDaOOCPruAoL5jdNfb4SyDrtvCKf4QS4G0rcPOacVRN2Cd_lr5XeAXoDy4kQJR2Z0d91Oa4LIRN2Cd_lryegQrJ6C6_l.">
<button class="buy-btn">立即购买</button>
</a>
</div>
</div>
<!-- 更多商品卡片... -->
<div class="goods-card" data-category="daily">
<div class="goods-img" style="background-image: url('../static/images/item3.png')">
<button class="favorite-btn">❤️ 收藏</button>
</div>
<div class="goods-info">
<h3>无线紧急呼叫器</h3>
<p class="goods-desc">千呼万唤,不如一按</p>
<div class="price-tag">
<span class="current-price">¥59</span>
</div>
<a
href="https://detail.tmall.com/item.htm?spm=a21n57.sem.item.97.5af23903hZxXlt&priceTId=2147801b17432969192931909e16f3&utparam=%7B%22aplus_abtest%22%3A%223604af41da8b23cbc9775683b3377884%22%7D&id=664135081185&ns=1&xxc=ad_ztc&pisk=gWUIcr4mEpvCybgJNDfZ1tmyNh3SJ17VOQG8i7Lew23py_wzQzo-UM2SV8yi2yuUTRM83RMpzgDeP4N7E8y7Z6c-e8eSUkSNuWVnr4B4FZ74tn2M5LyIpDnOwbhme1op0oebR4BV3atw6DXEyJy2fNs9XjDt9bnK91gtZXtKy0hL61G-apKJJ8C11bhjwenpvFntib0J2bH-WAhqwLhKvULO1bD9v438eVCsZAIFCfjIpW1KHskX3NRLQATJyPGOEmNB0EGUfXNshWUCa_5oODMY9A60SnirX7m7rQYS4WE0EjeppTMu55ULD-BwYvEjNSr7H_-n_0an5YUdoIMTR7asxlJpvjgIdmUKDa1bM0E_52zdEnVa1vnj7lSGL0uQdoDmvGXgF5MUeyi9dOkz05zKf-Bw58m72yuTJOQf4g8qhzuDVCiDPfMV11tkqAAFRJzc2uPIvfcau116d3mKsfMV11tkqDhi_m511ptl.">
<button class="buy-btn">立即购买</button>
</a>
</div>
</div>
</div>
</div>
<!-- 就业信息模块 -->
<div id="job-info" class="content-container">
<!-- 搜索筛选区 -->
<div class="job-filter">
<div class="search-box">
<input type="text" placeholder="搜索职位或公司..." class="search-input">
<a href="https://www.cdpee.org.cn/job/"><button class="search-btn">🔍 搜索</button></a>
</div>
</div>
<!-- 职位列表 -->
<div class="job-list">
<!-- 职位卡片1 -->
<div class="job-card">
<div class="job-header">
<h3>前端开发工程师</h3>
<span class="salary">¥15-25K</span>
</div>
<div class="job-meta">
<span class="company">ⓘ 无声科技</span>
<span class="location">📍 北京</span>
<span class="type">🏷️ 全职</span>
</div>
<div class="job-tags">
<span class="tag purple">手语面试</span>
<span class="tag purple">远程办公</span>
<span class="tag purple">弹性工时</span>
</div>
<div class="job-desc">
<p>岗位要求:</p>
<ul>
<li>精通HTML/CSS/JavaScript</li>
<li>熟悉无障碍开发标准</li>
<li>支持文字/视频沟通</li>
</ul>
</div>
<div class="job-actions">
<a href="https://www.cdpee.org.cn/job/">
<button class="apply-btn">立即申请</button></a>
<button class="save-btn">收藏职位</button>
</div>
</div>
<!-- 职位卡片2 -->
<div class="job-card highlighted">
<div class="job-header">
<h3>手语客服专员</h3>
<span class="salary">¥8-12K</span>
</div>
<div class="job-meta">
<span class="company">ⓘ 暖心服务</span>
<span class="location">📍 上海</span>
<span class="type">🏷️ 兼职</span>
</div>
<div class="job-tags">
<span class="tag purple">视频面试</span>
<span class="tag purple">岗前培训</span>
</div>
<div class="job-desc">
<p>岗位亮点:</p>
<ul>
<li>提供专业手语培训</li>
<li>弹性工作时段</li>
<li>在线工单系统</li>
</ul>
</div>
<div class="job-actions">
<a href="https://www.cdpee.org.cn/job/">
<button class="apply-btn">立即申请</button></a>
<button class="save-btn">收藏职位</button>
</div>
</div>
</div>
</div>
<div id="activity" class="content-container">
<!-- 原有的活动列表 -->
<div class="news-list">
<article class="news-item">
<div class="news-media">
<img src="../static/images/profile/family.jpg" class="news-image" alt="">
<div class="news-meta">
<time class="news-date">2025/02/14</time>
<span class="news-category">亲子家庭</span>
</div>
</div>
<div class="news-content">
<a href="http://iot.china.com.cn/content/2025-01/03/content_43004726.html"
style="text-decoration: none;">
<h2 class="news-title">亲子同乐,爱在手语间</h2>
<p class="news-desc">于龙东社区219号举办聋哑人家庭参与亲子游戏、手语互动等活动,增进家庭成员间的沟通与情感。</p>
</a>
</div>
</article>
<article class="news-item">
<div class="news-media">
<img src="../static/images/profile/game.jpg" class="news-image" alt="">
<div class="news-meta">
<time class="news-date">2025/01/15</time>
<span class="news-category">文化娱乐</span>
</div>
</div>
<div class="news-content">
<a href="https://www.hangzhou2022.cn/paragames/xw/ycydt/202308/t20230814_69562.shtml"
style="text-decoration: none;">
<h2 class="news-title">电竞无界,无声争锋</h2>
<p class="news-desc">本平台将举办聋哑人《英雄联盟》电子竞技比赛,欢迎报名参加!</p>
</a>
</div>
</article>
<article class="news-item">
<div class="news-media">
<img src="../static/images/profile/travel.jpg" class="news-image" alt="">
<div class="news-meta">
<time class="news-date">2025/01/01</time>
<span class="news-category">休闲娱乐</span>
</div>
</div>
<div class="news-content">
<a href="https://www.sohu.com/a/427336909_669645" style="text-decoration: none;">
<h2 class="news-title">走遍世界,无声同行</h2>
<p class="news-desc">走遍世界新一期旅行活动又来啦!这一次又将去哪里开启探索呢?</p>
</a>
</div>
</article>
<article class="news-item">
<div class="news-media">
<img src="../static/images/profile/baking.jpg" class="news-image" alt="">
<div class="news-meta">
<time class="news-date">2025/03/05</time>
<span class="news-category">技能培训</span>
</div>
</div>
<div class="news-content">
<a href="http://www.cjr.org.cn/info/notice/content/post_1148355.html"
style="text-decoration: none;">
<h2 class="news-title">甜蜜烘焙,无声创造</h2>
<p class="news-desc">无言公社将免费开设烘焙课程,教授聋哑人制作蛋糕、饼干等,心动就快来报名吧!</p>
</a>
</div>
</article>
<article class="news-item">
<div class="news-media">
<img src="images/profile/tree.jpeg" class="news-image" alt="">
<div class="news-meta">
<time class="news-date">2025/03/12</time>
<span class="news-category">公益志愿</span>
</div>
</div>
<div class="news-content">
<h2 class="news-title">绿色行动,无声守护</h2>
<p class="news-desc">植树节特别活动:快来一起种棵树吧~</p>
</div>
</article>
<article class="news-item">
<div class="news-media"></div>
<div class="news-content">
<h2>更多精彩活动,持续更新中……</h2>
</div>
</article>
</div>
</div>
<hr class="dashed">
<footer class="py-8 px-4 md:px-8 text-center fade-in" style='animation-delay:1.2s'>
<div class="flex flex-col items-center">
<div class="mb-4 text-lg font-medium">作者: backpack</div>
<p class="text-sm opacity-70">© 2025 WaveSign.
</div>
</footer>
<script>
function showContent(contentId) {
// 隐藏所有内容
document.querySelectorAll('.content-container').forEach(item => {
item.style.display = 'none';
});
// 显示指定内容
document.getElementById(contentId).style.display = 'block';
}
function initMap() {
const map = new AMap.Map('container', {
zoom: 13,
center: [121.4737, 31.2304]
});
// 添加交通态势图层
AMap.plugin('AMap.TileLayer.Traffic', () => {
map.add(new AMap.TileLayer.Traffic());
});
// 添加无障碍设施标记
const markers = [
{ position: [121.4752, 31.2315], title: '手语服务台' },
{ position: [121.4478, 31.2231], title: '无障碍电梯' }
];
markers.forEach(m => {
new AMap.Marker({
map: map,
position: m.position,
content: `<div class="access-marker">${m.title}</div>`
});
});
}
// 分类筛选功能
document.querySelectorAll('.category-btn').forEach(btn => {
btn.addEventListener('click', function () {
// 移除所有激活状态
document.querySelectorAll('.category-btn').forEach(b => b.classList.remove('active'));
this.classList.add('active');
// 获取分类
const category = this.dataset.category;
const goods = document.querySelectorAll('.goods-card');
goods.forEach(item => {
if (category === 'all' || item.dataset.category === category) {
item.style.display = 'block';
} else {
item.style.display = 'none';
}
});
});
});
// 搜索功能
document.querySelector('.search-btn').addEventListener('click', () => {
const keyword = document.querySelector('.search-input').value.toLowerCase();
document.querySelectorAll('.job-card').forEach(card => {
const text = card.textContent.toLowerCase();
card.style.display = text.includes(keyword) ? 'block' : 'none';
});
});
</script>
</body>
</html>

+ 270
- 0
templates/Login.html View File

@ -0,0 +1,270 @@
{% load static %}
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>手语通WaveSign - 登录</title>
<link href="https://fonts.googleapis.com/css2?family=Pacifico&family=Poppins:wght@400;600&display=swap"
rel="stylesheet">
<style>
▪ {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
min-height: 100vh;
background: linear-gradient(180deg, #d3c8e8 0%, #b762bf 100%);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 5%;
position: relative;
overflow: hidden;
}
/* 增强品牌标题 */
.brand-title {
position: absolute;
top: 3%;
left: 5%;
font-family: 'Pacifico', cursive;
font-size: 4.8rem;
/* 放大标题 */
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
z-index: 2;
}
.brand-title span:first-child {
color: #8a4fff;
}
.brand-title span:last-child {
color: #ffffff;
text-shadow:
-1px -1px 0 #8a4fff,
1px -1px 0 #cb6dea,
-1px 1px 0 #811e93,
1px 1px 0 #77639e;
}
/* 新增透视效果图片容器 */
.left-image {
flex: 1;
max-width: 800px;
padding-right: 60px;
margin-top: 80px;
transform: perspective(1500px) rotateY(-15deg);
transform-origin: left center;
}
.left-image img {
width: 90%;
height: auto;
filter: drop-shadow(0 0 30px rgba(138, 79, 255, 0.3));
transform: scale(1.1);
}
/* 增强登录表单 */
.login-container {
background: rgba(255, 255, 255, 0.97);
padding: 40px;
border-radius: 20px;
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.15);
width: 400px;
margin-right: 5%;
backdrop-filter: blur(5px);
}
h1 {
color: #666;
margin-bottom: 30px;
text-align: center;
font-family: 'Poppins', sans-serif;
font-weight: 600;
font-size: 2rem;
/* 放大表单标题 */
}
/* 新增表单样式 */
.form-group {
margin-bottom: 20px;
}
input[type="text"],
input[type="password"] {
width: 90%;
padding: 12px 20px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 16px;
transition: all 0.3s;
}
input:focus {
border-color: #bf6bd0;
box-shadow: 0 0 8px rgba(138, 79, 255, 0.2);
}
.remember-me {
display: flex;
align-items: center;
margin: 15px 0;
color: #666;
}
button {
width: 100%;
padding: 14px;
background: linear-gradient(135deg, #b952d5, #b84ebe);
color: rgb(100, 65, 95);
border: none;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: transform 0.2s;
}
button:hover {
transform: translateY(-2px);
}
.register-section {
margin-top: 25px;
text-align: center;
padding-top: 20px;
border-top: 1px solid #eee;
}
.register-text {
color: #666;
font-size: 16px;
margin-right: 8px;
}
.register-link {
color: #8a4fff;
text-decoration: none;
font-weight: 600;
background: linear-gradient(120deg, #8a4fff, #b762bf);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
transition: all 0.3s;
font-size: 16px;
}
.register-link:hover {
text-shadow: 0 2px 5px rgba(138, 79, 255, 0.3);
transform: translateY(-1px);
}
@media (max-width: 768px) {
.brand-title {
font-size: 2.5rem;
top: 3%;
}
.left-image {
transform: none;
padding-right: 20px;
}
.left-image img {
width: 100%;
}
}
.message-container {
margin-bottom: 20px;
}
.message {
padding: 12px 20px;
border-radius: 8px;
font-family: 'Poppins', sans-serif;
font-size: 14px;
margin-bottom: 10px;
text-align: center;
animation: fade-in 0.4s ease-out;
}
.message.error {
background-color: #ffe5e5;
color: #d60000;
border: 1px solid #f5c2c7;
}
.message.success {
background-color: #e9f6ea;
color: #3c763d;
border: 1px solid #b4e2be;
}
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(-5px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>
</head>
<body>
<div class="brand-title">
<span>Sign</span><span>Link</span>
</div>
<div class="left-image">
<img src="{% static 'images/登录1.jpg' %}" alt="图片">
</div>
<div class="login-container">
<h1>欢迎登录 WaveSign</h1>
{% if messages %}
<div class="message-container">
{% for message in messages %}
<div class="message {{ message.tags }}">
{{ message }}
</div>
{% endfor %}
</div>
{% endif %}
<form method="post" action="{% url 'login' %}">
{% csrf_token %}
<div class="form-group">
<input type="text" name="username" placeholder="请输入账号" required>
</div>
<div class="form-group">
<input type="password" name="password" placeholder="请输入密码" required>
</div>
<div class="remember-me">
<input type="checkbox" id="remember">
<label for="remember">记住密码</label>
</div>
<button type="submit">立即登录</button>
<div class="register-section">
<span class="register-text">没有账号?</span>
<a href="{% url 'register' %}" class="register-link">立即注册</a>
</div>
</form>
</div>
<script>
function validateForm() {
// 这里可以添加实际验证逻辑
window.location.href = "{% url 'home' %}";
return false;
}
</script>
</body>
</html>

+ 238
- 0
templates/Register.html View File

@ -0,0 +1,238 @@
{% load static %}
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>手语通WaveSign - 注册</title>
<link href="https://fonts.googleapis.com/css2?family=Pacifico&family=Poppins:wght@400;600&display=swap"
rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
min-height: 100vh;
background: linear-gradient(180deg, #d3c8e8 0%, #b762bf 100%);
display: flex;
justify-content: center;
/* 修改为居中布局 */
align-items: center;
position: relative;
}
.brand-title {
position: absolute;
top: 3%;
left: 15%;
transform: translateX(-50%);
font-family: 'Pacifico', cursive;
font-size: 4.8rem;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
z-index: 2;
white-space: nowrap;
}
.brand-title span:first-child {
color: #8a4fff;
}
.brand-title span:last-child {
color: #ffffff;
text-shadow:
-1px -1px 0 #8a4fff,
1px -1px 0 #cb6dea,
-1px 1px 0 #811e93,
1px 1px 0 #77639e;
}
.register-container {
background: rgba(255, 255, 255, 0.97);
padding: 40px;
border-radius: 20px;
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.15);
width: 450px;
backdrop-filter: blur(5px);
margin-top: 80px;
/* 为品牌标题留出空间 */
}
h1 {
color: #666;
margin-bottom: 30px;
text-align: center;
font-family: 'Poppins', sans-serif;
font-weight: 600;
font-size: 2rem;
}
.form-group {
margin-bottom: 20px;
}
input[type="text"],
input[type="email"],
input[type="password"] {
width: 100%;
padding: 12px 20px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 16px;
transition: all 0.3s;
}
input:focus {
border-color: #bf6bd0;
box-shadow: 0 0 8px rgba(138, 79, 255, 0.2);
}
.password-requirements {
color: #666;
font-size: 12px;
margin: 10px 0;
padding-left: 10px;
}
.terms {
margin: 15px 0;
font-size: 13px;
color: #666;
display: flex;
align-items: center;
}
.terms input[type="checkbox"] {
margin-right: 8px;
}
.terms a {
color: #8a4fff;
text-decoration: none;
margin-left: 5px;
}
.errorlist {
color: #dc3545;
list-style: none;
padding: 10px 0;
font-size: 14px;
}
button[type="submit"] {
width: 100%;
padding: 14px;
background: linear-gradient(135deg, #b952d5, #b84ebe);
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: transform 0.2s;
font-family: 'Poppins', sans-serif;
}
button[type="submit"]:hover {
transform: translateY(-2px);
}
@media (max-width: 768px) {
.brand-title {
font-size: 2.5rem;
top: 5%;
}
.register-container {
width: 90%;
margin: 80px 5% 0;
padding: 25px;
}
h1 {
font-size: 1.8rem;
}
}
@media (max-width: 480px) {
.brand-title {
font-size: 2rem;
}
input[type="text"],
input[type="email"],
input[type="password"] {
padding: 10px 15px;
font-size: 14px;
}
button[type="submit"] {
padding: 12px;
font-size: 14px;
}
}
</style>
</head>
<body>
<div class="brand-title">
<span>Sign</span><span>Link</span>
</div>
<div class="register-container">
<h1>加入 WaveSign 社区</h1>
<form method="post">
{% csrf_token %}
<!-- 用户名 -->
<div class="form-group">
{{ form.username }}
{% for error in form.username.errors %}
<div class="errorlist">{{ error }}</div>
{% endfor %}
<div class="hint-text">(4-16位字符)</div>
</div>
<!-- 邮箱 -->
<div class="form-group">
{{ form.email }}
{% for error in form.email.errors %}
<div class="errorlist">{{ error }}</div>
{% endfor %}
</div>
<!-- 密码 -->
<div class="form-group">
{{ form.password }}
{% for error in form.password.errors %}
<div class="errorlist">{{ error }}</div>
{% endfor %}
<div class="password-requirements">✓ 密码必须为至少8位的字母和数字组合</div>
</div>
<!-- 确认密码 -->
<div class="form-group">
{{ form.password_confirm }}
{% for error in form.password_confirm.errors %}
<div class="errorlist">{{ error }}</div>
{% endfor %}
</div>
<!-- 同意条款 -->
<div class="terms">
{{ form.agreed_terms }}
<label for="{{ form.agreed_terms.id_for_label }}">我已阅读并同意 <a href="#">服务条款</a></label>
{% for error in form.agreed_terms.errors %}
<div class="errorlist">{{ error }}</div>
{% endfor %}
</div>
<button type="submit">创建账号</button>
</form>
</div>
</body>
</html>

+ 389
- 0
templates/SLClassroom.html View File

@ -0,0 +1,389 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>手语教室</title>
<link rel="stylesheet" href="{% static 'css/SLClassroom.css' %}">
<link rel="stylesheet" href="{% static 'css/login_button.css' %}">
<!-- 引入 Font Awesome 图标库 -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css" rel="stylesheet">
<script src=https://cdn.tailwindcss.com></script>
<!-- mediapipe -->
<script type="module" src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="{% static 'js/SLClassroom.js' %}"></script>
<script type="module" src="{% static 'js/upload_tasks_evaluator.js' %}"></script>
</head>
<body>
<!-- 导航栏 -->
<header class="py-6 px-4 md:px-8 flex justify-between items-center">
<div class="absolute left-1/2 transform -translate-x-1/2">
<a class="text-3xl md:text-4xl font-bold fade-in">WaveSign手语通</a>
<p class="text-lg md:text-xl opacity-80 fade-in mt-2">连接双手与世界,构建无声的桥梁</p>
</div>
<div class="flex items-center space-x-4 ml-auto">
{% if user.is_authenticated %}
<div class="user-menu-box-container">
<a href="{% url 'my_page' %}" class="user-menu-box-button">
{% if user.avatar %}
<img src="{{ user.avatar.url }}" alt="头像" class="user-menu-box-avatar">
{% else %}
<img src="{% static 'images/default_avatar.png' %}" alt="默认头像" class="user-menu-box-avatar">
{% endif %}
{{ user.username }}
</a>
<div class="user-menu-box-dropdown">
<a href="{% url 'my_page' %}">进入个人主页</a>
<a href="{% url 'logout' %}">退出登录</a>
</div>
</div>
{% else %}
<a href="{% url 'login' %}" class="user-menu-box-login">
<i class="fas fa-user"></i> 登录
</a>
{% endif %}
</div>
</header>
<nav class="sticky top-0 bg-opacity-90 backdrop-blur-sm py-4 px-4 md:px-8 z-50 fade-in"
style=background-color:var(--card-bg)>
<div class="container mx-auto flex justify-center space-x-8">
<a class="nav-link text-lg font-medium" href="{% url 'home' %}" style="color:var(--text-color)">首页</a>
<a class="nav-link text-lg font-medium" href="{% url 'sl_classroom' %}"
style="color:var(--text-color)">手语教室</a>
<a class="nav-link text-lg font-medium" href="{% url 'life_serving' %}"
style="color:var(--text-color)">生活服务</a>
<a class="nav-link text-lg font-medium" href="{% url 'community' %}"
style="color:var(--text-color)">手语社区</a>
<a class="nav-link text-lg font-medium" href="{% url 'schedule' %}" style="color:var(--text-color)">日程管理</a>
<a class="nav-link text-lg font-medium" href="{% url 'my_page' %}" style="color:var(--text-color)">我的</a>
</div>
</nav>
<nav class="navbar">
<div class="logo">
<span class="title-highlight">🤟 手语教室</span>
</div>
<div class="nav-divider"></div>
<div class="icon-container">
<!-- 学习进度图标 -->
<div class="hover-container">
<div class="icon-circle">
<span class="navbaricon">
<i class="fas fa-chart-line text-purple-700 text-5xl hover:text-purple-500 transition-all"></i>
</span>
</div>
<div class="hover-card">
<h3>学习进度</h3>
<p>你已经完成了<span class="font-bold">75%</span>的基础手语课程!</p>
<div class="progress-bar">
<div class="progress" style="width: 75%;"></div>
</div>
<p>继续努力,你距离下一个奖励只差一步!</p>
</div>
</div>
<!-- 我的积分图标 -->
<div class="hover-container">
<div class="icon-circle">
<span class="navbaricon">
<i class="fas fa-gem text-purple-700 text-5xl hover:text-purple-500 transition-all"></i>
</span>
</div>
<div class="hover-card">
<h3>我的积分</h3>
<p>你目前拥有 <span class="font-bold">1200</span> 积分!</p>
<p>完成更多任务,获得更多积分!</p>
<div class="bonus-animations">
<span>积分即将加倍!</span>
<div class="gem-spin">
<i class="fas fa-gem text-yellow-500 text-4xl spin"></i>
</div>
</div>
</div>
</div>
</div>
</nav>
<!-- 学习模块 -->
<div class="content">
<!-- 侧边栏按钮部分 -->
<div class="tab">
<button class="tablinks" onclick="openTab(event, 'videoPractice')" id="defaultOpen">
<div class="tab-content flex items-center mb-4">
<div>
<i class="fas fa-book text-purple-600 dark:text-purple-400 text-xl"></i>
<h3 class="text-xl font-semibold">镜像视频练习</h3>
</div>
</div>
</button>
<button class="tablinks" onclick="openTab(event, 'signLanguageMap')">
<div class="tab-content flex items-center mb-4">
<div>
<i class="fas fa-book text-purple-600 dark:text-purple-400 text-xl"></i>
<h3 class="text-xl font-semibold">基础手语课程</h3>
</div>
</div>
</button>
<button class="tablinks" onclick="openTab(event, 'videoLessons')">
<div class="tab-content flex items-center mb-4">
<div>
<i class="fas fa-video text-purple-600 dark:text-purple-400 text-xl"></i>
<h3 class="text-xl font-semibold">视频教学</h3>
</div>
</div>
</button>
<button class="tablinks" onclick="openTab(event, 'practiceSection')">
<div class="tab-content flex items-center mb-4">
<div>
<i class="fas fa-comments text-purple-600 dark:text-purple-400 text-xl"></i>
<h3 class="text-xl font-semibold">互动练习</h3>
</div>
</div>
</button>
</div>
<!-- 选项卡内容部分 -->
<div class="tabcontent" id="videoPractice">
<h3 class="text-3xl font-bold mb-6 text-center">📹 上传手语视频进行匹配评分</h3>
<div class="max-w-4xl mx-auto space-y-6 bg-white p-6 rounded-xl shadow-lg">
<!-- 手势选择下拉 -->
<div class="my-4 flex items-center gap-4">
<label for="gestureSelect" class="font-medium">选择标准手势:</label>
<select id="gestureSelect" class="px-3 py-1 border rounded text-sm">
<option value="你好">你好</option>
<option value="早上好">早上好</option>
<option value="谢谢">谢谢</option>
<!-- 你可以继续添加其他手势选项 -->
</select>
</div>
<!-- ✅ 标准参考区 -->
<div class="flex flex-wrap md:flex-nowrap gap-6 my-6">
<!-- 标准 Canvas 动画 -->
<div>
<h3 class="text-lg font-bold mb-2">🎯 标准手势预览</h3>
<canvas id="standardCanvas" width="320" height="320" class="border rounded shadow"></canvas>
<button id="replayBtn" class="mt-2 bg-blue-500 text-white px-4 py-1 rounded">▶️ 重播手势动画</button>
</div>
<!-- 标准视频窗口 -->
<div class="flex-1">
<h3 class="text-lg font-bold mb-2">🎥 标准示范视频</h3>
<video id="standardVideo" class="w-full rounded shadow" controls></video>
</div>
</div>
<!-- ✅ 模式选择卡片 -->
<div class="flex justify-center gap-6 mb-6" id="modeCards">
<div id="card-video"
class="mode-card border rounded-lg p-4 shadow cursor-pointer bg-white text-center transition hover:ring-2 hover:ring-blue-500">
<div class="text-2xl">📤</div>
<div class="mt-2 font-semibold text-gray-800">上传视频评分</div>
</div>
<div id="card-live"
class="mode-card border rounded-lg p-4 shadow cursor-pointer bg-white text-center transition hover:ring-2 hover:ring-blue-500">
<div class="text-2xl">📷</div>
<div class="mt-2 font-semibold text-gray-800">实时摄像头评分</div>
</div>
</div>
<!-- ✅ 实时摄像头控件 -->
<div id="liveSection" class="hidden">
<div class="my-6">
<h3 class="text-lg font-bold mb-2">📷 实时摄像头预览</h3>
<video id="liveVideo" autoplay muted playsinline style="transform: scaleX(-1);"
class="rounded-xl shadow w-full max-w-md"></video>
<button id="startLiveBtn" class="mt-2 bg-green-500 text-white px-4 py-2 rounded">开始实时匹配</button>
<button id="stopLiveBtn"
class="mt-2 bg-gray-400 text-white px-4 py-2 rounded hidden">停止</button>
<div id="liveFeedback" class="mt-2 text-sm font-medium"></div>
</div>
</div>
<!-- ✅ 视频上传控件 -->
<div id="uploadSection">
<div class="my-4">
<label class="block mb-2 text-sm font-medium text-gray-700">📤 上传手语视频</label>
<input type="file" id="videoUpload" accept="video/mp4"
class="block w-full text-sm text-gray-600 border rounded p-2 shadow" />
</div>
<!-- ✅ 匹配反馈区域 -->
<div id="feedback"
class="text-center text-sm font-medium text-gray-700 bg-gray-100 py-2 px-4 rounded-full w-fit mx-auto shadow">
请上传视频后等待评分...
</div>
<!-- ✅ 视频播放和隐藏分析用容器 -->
<video id="uploadedVideo" class="w-full bg-gray-200 rounded-xl mt-4" controls></video>
<canvas id="hiddenCanvas" width="640" height="480" class="hidden"></canvas>
<!-- ✅ 高得分帧展示卡片 -->
<div class="mt-8">
<h3 class="text-lg font-bold mb-2 text-center">✨ 匹配最佳的用户帧展示</h3>
<div id="topFramesContainer" class="flex flex-wrap justify-center gap-6"></div>
</div>
</div>
</div>
</div>
<div class="tabcontent" id="signLanguageMap">
<h3>🌍 探索手语世界</h3>
<p>完成每个关卡,逐步解锁手语技能!</p>
<div class="map-container">
<!-- 已解锁关卡 -->
<div class="map-point unlocked" onclick="openLesson(1)">
<span class="emoji">👋</span>
<span class="lesson-name">问候语</span>
</div>
<div class="progress-line unlocked-line"></div>
<div class="map-point unlocked" onclick="openLesson(2)">
<span class="emoji"></span>
<span class="lesson-name">日常表达</span>
</div>
<div class="progress-line locked-line"></div>
<!-- 未解锁关卡 -->
<div class="map-point locked">
<span class="emoji"></span>
<span class="lesson-name">家庭成员</span>
</div>
<div class="progress-line locked-line"></div>
<div class="map-point locked">
<span class="emoji"></span>
<span class="lesson-name">食物饮料</span>
</div>
<div class="progress-line locked-line"></div>
<div class="map-point locked">
<span class="emoji"></span>
<span class="lesson-name">日常对话</span>
</div>
</div>
</div>
<div class="tabcontent" id="videoLessons">
<h3>📚 手语课程视频</h3>
<p>欢迎来到手语教学课堂!点击视频进行学习,完成后可解锁新的内容。</p>
<div class="lesson-container">
<div class="lesson-card completed">
<div class="lesson-header">
<span class="lesson-title">第一课:你好</span>
<i class="fas fa-check-circle completed-icon"></i>
</div>
<iframe src="https://player.bilibili.com/player.html?bvid=BV1LW411s7gR&page=1" width="100%"
height="315" frameborder="0" allowfullscreen></iframe>
<div class="lesson-progress">
<div class="progress-bar" style="width: 100%;"></div>
</div>
</div>
<div class="lesson-card in-progress">
<div class="lesson-header">
<span class="lesson-title">第二课:谢谢</span>
<i class="fas fa-hourglass-half progress-icon"></i>
</div>
<iframe src="https://player.bilibili.com/player.html?bvid=BV1LW411s7gR&page=1" width="100%"
height="315" frameborder="0" allowfullscreen></iframe>
<div class="lesson-progress">
<div class="progress-bar" style="width: 50%;"></div>
</div>
</div>
<div class="lesson-card locked">
<div class="lesson-header">
<span class="lesson-title">第三课:再见</span>
<i class="fas fa-lock lock-icon"></i>
</div>
<p class="locked-message">完成前面课程即可解锁!</p>
</div>
</div>
</div>
<div class="tabcontent" id="practiceSection">
<h3>✋ 互动练习</h3>
<p>点击卡片查看手语的正确含义!</p>
<div class="practice-container">
<div class="practice-card" onclick="flipCard(this)">
<div class="card-front">
<img src="{% static 'images/sign_you.png' %}" alt="手语示例">
<span>这个手势的含义是?</span>
</div>
<div class="card-back">
<span>👉 你</span>
</div>
</div>
<div class="practice-card" onclick="flipCard(this)">
<div class="card-front">
<img src="{% static 'images/sign_good.png'%}" alt="手语示例">
<span>这个手势的含义是?</span>
</div>
<div class="card-back">
<span>✨ 好</span>
</div>
</div>
<div class="practice-card" onclick="flipCard(this)">
<div class="card-front">
<img src="{% static 'images/sign_thank_you.png'%}" alt="手语示例">
<span>这个手势的含义是?</span>
</div>
<div class="card-back">
<span>🙏 谢谢!</span>
</div>
</div>
</div>
</div>
</div>
<footer class="py-8 px-4 md:px-8 text-center fade-in" style=animation-delay:1.2s>
<div class="flex flex-col items-center">
<div class="mb-4 text-lg font-medium">作者: backpack</div>
<p class="text-sm opacity-70">© 2025 WaveSign.
</div>
</footer>
</div>
</body>
<script src="{% static 'js/SLClassroom.js' %}"></script>
</body>

+ 983
- 0
templates/Schedule.html View File

@ -0,0 +1,983 @@
{% load static %}
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WaveSign手语通 - 日程管理</title>
<link rel="stylesheet" href="{% static 'css/login_button.css' %}">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css">
<script src="https://cdn.tailwindcss.com"></script>
<script>
// 配置Tailwind自定义颜色
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#8d6a95',
secondary: '#cebbf3',
dark: '#1a1a1a',
light: '#f9f9f9',
},
}
}
}
</script>
<style type="text/tailwindcss">
:root {
--primary: #8d6a95;
--primary-light: #cebbf3;
--bg-color: #f9f9f9;
--card-bg: #fff;
--text-color: hsl(301, 89%, 21%);
--border-color: #e5e5e5;
}
body {
background-color: var(--bg-color);
color: var(--text-color);
font-family: 'Noto Sans SC', sans-serif;
transition: background-color .3s, color .3s;
padding: 2rem 5%;
max-width: 1440px;
margin: 0 auto;
box-sizing: border-box;
}
@media (min-width: 1600px) {
body {
padding: 2rem 8%;
}
}
@media (max-width: 768px) {
body {
padding: 1.5rem;
}
}
.main-container {
padding: 0 2rem;
margin: 0 auto;
}
.card {
background-color: var(--card-bg);
border: 1px solid var(--border-color);
transition: transform .3s, box-shadow .3s, border-color .3s;
margin: 1rem 0;
padding: 1.5rem;
}
.card:hover {
border-color: var(--primary);
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
}
.btn {
transition: transform .2s, background-color .2s;
}
.btn:hover {
transform: scale(1.05);
}
.fade-in {
animation: .8s ease-in fadeIn;
}
@keyframes fadeIn {
0% {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.nav-link {
position: relative;
}
.nav-link:after {
content: "";
background-color: var(--primary);
width: 0;
height: 2px;
transition: width .3s;
position: absolute;
bottom: -2px;
left: 0;
}
.nav-link:hover:after,
.nav-link.active:after {
width: 100%;
}
/* 日历专用样式 */
.calendar-day {
min-height: 100px;
border: 1px solid var(--border-color);
border-radius: 0.25rem;
padding: 0.5rem;
transition: all 0.2s ease;
}
.calendar-day:hover {
background-color: rgba(206, 187, 243, 0.1);
}
.day-header {
background-color: var(--card-bg);
font-weight: bold;
text-align: center;
padding: 0.5rem;
border-radius: 0.25rem;
}
.day-number {
font-size: 1.2rem;
font-weight: 600;
color: var(--primary);
}
.event-marker {
position: absolute;
bottom: 0.5rem;
left: 0.5rem;
right: 0.5rem;
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
}
.event-dot {
width: 0.5rem;
height: 0.5rem;
border-radius: 9999px;
background-color: var(--primary);
}
</style>
</head>
<body >
<header class="py-6 px-4 md:px-8 flex justify-between items-center">
<div class="absolute left-1/2 transform -translate-x-1/2">
<a class="text-3xl md:text-4xl font-bold fade-in">WaveSign手语通</a>
<p class="text-lg md:text-xl opacity-80 fade-in mt-2">连接双手与世界,构建无声的桥梁</p>
</div>
<div class="flex items-center space-x-4 ml-auto">
<div class="flex items-center space-x-4 ml-auto">
{% if user.is_authenticated %}
<div class="user-menu-box-container">
<a href="{% url 'my_page' %}" class="user-menu-box-button">
{% if user.avatar %}
<img src="{{ user.avatar.url }}" alt="头像" class="user-menu-box-avatar">
{% else %}
<img src="{% static 'images/default_avatar.png' %}" alt="默认头像" class="user-menu-box-avatar">
{% endif %}
{{ user.username }}
</a>
<div class="user-menu-box-dropdown">
<a href="{% url 'my_page' %}">进入个人主页</a>
<a href="{% url 'logout' %}">退出登录</a>
</div>
</div>
{% else %}
<a href="{% url 'login' %}" class="user-menu-box-login">
<i class="fas fa-user"></i> 登录
</a>
{% endif %}
</div>
</header>
<nav class="sticky top-0 bg-opacity-90 backdrop-blur-sm py-4 px-4 md:px-8 z-50 fade-in" style="background-color:var(--card-bg)">
<div class="container mx-auto flex justify-center space-x-8">
<a class="nav-link text-lg font-medium" href="{% url 'home' %}" style="color:var(--text-color)">首页</a>
<a class="nav-link text-lg font-medium" href="{% url 'sl_classroom' %}" style="color:var(--text-color)">手语教室</a>
<a class="nav-link text-lg font-medium" href="{% url 'life_serving' %}" style="color:var(--text-color)">生活服务</a>
<a class="nav-link text-lg font-medium" href="{% url 'community' %}" style="color:var(--text-color)">手语社区</a>
<a class="nav-link text-lg font-medium" href="{% url 'schedule' %}" style="color:var(--text-color)">日程管理</a>
<a class="nav-link text-lg font-medium" href="{% url 'my_page' %}" style="color:var(--text-color)">我的</a>
</div>
</nav>
<!-- 主内容区 -->
<main class="container mx-auto px-4 py-8">
<div class="mb-8 fade-in">
<h2 class="text-[clamp(1.5rem,3vw,2.5rem)] font-bold text-primary flex items-center">
<i class="fa fa-calendar-check-o mr-3"></i>日程管理
</h2>
</div>
<!-- 日历容器 -->
<div class="card rounded-xl hover:shadow-lg mb-8">
<div class="flex justify-between items-center mb-6">
<h3 class="text-xl font-bold text-primary flex items-center">
<i class="fa fa-calendar mr-2"></i>我的日历
</h3>
<div class="flex space-x-2">
<button onclick="prevMonth()" class="p-2 rounded-full hover:bg-primary-light/50 transition-colors">
<i class="fa fa-chevron-left text-primary"></i>
</button>
<button onclick="goToToday()" class="px-4 py-2 bg-primary text-white rounded-full hover:bg-primary-light transition-colors">
今天
</button>
<button onclick="nextMonth()" class="p-2 rounded-full hover:bg-primary-light/50 transition-colors">
<i class="fa fa-chevron-right text-primary"></i>
</button>
</div>
</div>
<div class="mb-4">
<h4 id="currentMonthDisplay" class="text-2xl font-bold text-center text-primary"></h4>
</div>
<div id="calendar" class="mb-8"></div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6">
<!-- 待办事项区域 -->
<div class="lg:col-span-5 card rounded-xl hover:shadow-lg">
<div class="flex justify-between items-center mb-6">
<h3 class="text-xl font-bold text-primary flex items-center">
<i class="fa fa-tasks mr-2"></i>待办清单
</h3>
<div class="relative">
<span id="taskCount" class="bg-primary text-white text-xs rounded-full px-2 py-1">0</span>
</div>
</div>
<div class="mb-4">
<div class="flex space-x-2">
<input type="text" id="newTaskInput" placeholder="添加新任务..."
class="flex-1 px-4 py-2 border border-light rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/50 bg-transparent text-black">
<button id="addTaskBtn" class="bg-primary text-black px-4 py-2 rounded-lg hover:bg-primary-light transition-colors">
<i class="fa fa-plus"></i>
</button>
</div>
</div>
<div id="taskList" class="space-y-2 max-h-[500px] overflow-y-auto pr-2">
<!-- 待办事项将通过JavaScript动态生成 -->
<div class="text-center py-8 text-gray-400">
<i class="fa fa-check-circle text-4xl mb-3"></i>
<p>暂无待办事项</p>
</div>
</div>
</div>
<!-- 日程表单区域 -->
<div class="lg:col-span-7 card rounded-xl hover:shadow-lg">
<h3 class="text-xl font-bold text-primary flex items-center mb-6">
<i class="fa fa-plus-circle mr-2"></i>添加日程
</h3>
<form id="eventForm" class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="form-group">
<label for="eventTitle" class="block text-sm font-medium text-gray-700 mb-1">事件名称</label>
<input type="text" id="eventTitle" class="w-full px-4 py-2 border border-light rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/50 bg-transparent text-black" placeholder="输入事件名称">
</div>
<div class="form-group">
<label for="eventDate" class="block text-sm font-medium text-gray-700 mb-1">日期</label>
<input type="date" id="eventDate" class="w-full px-4 py-2 border border-light rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/50 bg-transparent text-black">
</div>
<div class="form-group">
<label for="eventTime" class="block text-sm font-medium text-gray-700 mb-1">时间</label>
<input type="time" id="eventTime" class="w-full px-4 py-2 border border-light rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/50 bg-transparent text-black">
</div>
<div class="form-group md:col-span-3">
<label for="eventDescription" class="block text-sm font-medium text-gray-700 mb-1">描述</label>
<textarea id="eventDescription" rows="3" class="w-full px-4 py-2 border border-light rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/50 bg-transparent text-black" placeholder="输入事件描述(可选)"></textarea>
</div>
<div class="form-group md:col-span-3 flex justify-end">
<button type="button" id="submitEventBtn" class="bg-primary text-white px-6 py-3 rounded-lg hover:bg-primary-light transition-colors flex items-center">
<i class="fa fa-calendar-plus-o mr-2"></i>添加日程
</button>
</div>
</form>
</div>
</div>
<!-- 事件模态框 -->
<div id="eventModal" class="fixed inset-0 bg-black bg-opacity-50 hidden flex items-center justify-center z-50">
<div id="eventModalContent" class="bg-white rounded-lg p-6 max-w-lg w-full mx-4">
<!-- 模态框内容将由 JavaScript 动态填充 -->
</div>
</div>
<!-- 添加事件按钮 -->
<button onclick="showAddEventModal()" class="fixed bottom-8 right-8 bg-primary text-white p-4 rounded-full shadow-lg hover:bg-primary-dark transition-colors">
<i class="fa fa-plus"></i>
</button>
<!-- 添加事件模态框 -->
<div id="addEventModal" class="fixed inset-0 bg-black bg-opacity-50 hidden flex items-center justify-center z-50">
<div class="bg-white rounded-lg p-6 max-w-lg w-full mx-4">
<h3 class="text-xl font-bold mb-4">添加新事件</h3>
<form id="addEventForm" onsubmit="addEvent(); return false;">
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2" for="eventTitle">
事件名称
</label>
<input type="text" id="eventTitle" name="title" required
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2" for="eventDate">
日期
</label>
<input type="date" id="eventDate" name="date" required
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2" for="eventTime">
时间
</label>
<input type="time" id="eventTime" name="time" required
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2" for="eventDescription">
描述
</label>
<textarea id="eventDescription" name="description"
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"></textarea>
</div>
<div class="flex justify-end space-x-2">
<button type="button" onclick="closeAddEventModal()"
class="px-4 py-2 bg-gray-500 text-white rounded hover:bg-gray-600">取消</button>
<button type="submit"
class="px-4 py-2 bg-primary text-white rounded hover:bg-primary-dark">保存</button>
</div>
</form>
</div>
</div>
</main>
<footer class="py-8 px-4 md:px-8 text-center fade-in">
<div class="flex flex-col items-center">
<div class="mb-4 text-lg font-medium">作者: backpack</div>
<p class="text-sm opacity-70">© 2025 WaveSign.</p>
</div>
</footer>
<script>
// 获取CSRF Token
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
// 显示添加事件模态框
function showAddEventModal() {
const modal = document.getElementById('addEventModal');
modal.classList.remove('hidden');
}
// 关闭添加事件模态框
function closeAddEventModal() {
const modal = document.getElementById('addEventModal');
modal.classList.add('hidden');
}
// 显示通知
function showNotification(message) {
const notification = document.createElement('div');
notification.className = 'fixed bottom-4 right-4 bg-primary text-white px-4 py-2 rounded-lg shadow-lg z-50 fade-in';
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.style.opacity = '0';
notification.style.transform = 'translateY(10px)';
notification.style.transition = 'opacity 0.3s, transform 0.3s';
setTimeout(() => {
notification.remove();
}, 300);
}, 3000);
}
// 格式化日期为 YYYY-MM-DD
function formatDate(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
// 当前日期
const today = new Date();
let currentDate = new Date(today.getFullYear(), today.getMonth(), today.getDate());
// 获取月数据
function fetchMonthData(date) {
const year = date.getFullYear();
const month = date.getMonth();
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
fetch(`/Schedule/events/?start_date=${formatDate(firstDay)}&end_date=${formatDate(lastDay)}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken')
},
credentials: 'include'
})
.then(response => {
if (response.status === 401) {
throw new Error('请先登录');
}
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
// 检查数据格式
if (!data || typeof data !== 'object') {
throw new Error('Invalid data format received from server');
}
// 确保 data 是数组
const eventsArray = Array.isArray(data) ? data : [];
// 将事件数据按日期分组
const eventsByDate = {};
eventsArray.forEach(event => {
if (event && event.date) {
const date = event.date;
if (!eventsByDate[date]) {
eventsByDate[date] = [];
}
eventsByDate[date].push(event);
}
});
// 渲染日历
renderCalendar(eventsByDate);
})
.catch(error => {
console.error('Error fetching month data:', error);
const calendar = document.getElementById('calendar');
if (calendar) {
if (error.message === '请先登录') {
calendar.innerHTML = `
<div class="col-span-7 text-center py-10 text-red-500">
<i class="fa fa-exclamation-triangle text-2xl mb-2"></i>
<p>请先登录</p>
<p class="text-sm mt-2">您需要登录才能查看日程</p>
<a href="{% url 'login' %}" class="inline-block mt-4 px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-light transition-colors">
去登录
</a>
</div>
`;
} else {
calendar.innerHTML = `
<div class="col-span-7 text-center py-10 text-red-500">
<i class="fa fa-exclamation-triangle text-2xl mb-2"></i>
<p>加载日历数据失败</p>
<p class="text-sm mt-2">${error.message}</p>
<p class="text-sm mt-2">请检查后端服务是否正常运行</p>
</div>
`;
}
}
});
}
// 渲染日历
function renderCalendar(eventsByDate = {}) {
const calendar = document.getElementById('calendar');
if (!calendar) {
console.error('Calendar container not found');
return;
}
calendar.innerHTML = '';
// 更新月份显示
const monthNames = ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'];
document.getElementById('currentMonthDisplay').textContent =
`${currentDate.getFullYear()}年${monthNames[currentDate.getMonth()]}`;
// 添加星期头部
const weekDays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
const weekHeader = document.createElement('div');
weekHeader.className = 'grid grid-cols-7 gap-2 mb-2';
weekDays.forEach(day => {
const dayHeader = document.createElement('div');
dayHeader.className = 'day-header text-center py-2';
if (day === '周六' || day === '周日') {
dayHeader.classList.add('text-amber-600');
}
dayHeader.textContent = day;
weekHeader.appendChild(dayHeader);
});
calendar.appendChild(weekHeader);
// 计算当月第一天是星期几 (0-6, 0是星期日)
const firstDay = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1);
const lastDay = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0);
const startingDay = firstDay.getDay() || 7; // 调整为星期一为第一天
const totalDays = lastDay.getDate();
// 创建日历网格
const calendarGrid = document.createElement('div');
calendarGrid.className = 'grid grid-cols-7 gap-2';
// 添加上个月的占位日期
for (let i = 1; i < startingDay; i++) {
const emptyDay = document.createElement('div');
emptyDay.className = 'calendar-day text-gray-300';
calendarGrid.appendChild(emptyDay);
}
// 添加当月的日期
for (let day = 1; day <= totalDays; day++) {
const date = new Date(currentDate.getFullYear(), currentDate.getMonth(), day);
const dateStr = formatDate(date);
const dayCell = document.createElement('div');
dayCell.className = 'calendar-day relative';
// 检查是否是今天
const isToday = (
day === today.getDate() &&
currentDate.getMonth() === today.getMonth() &&
currentDate.getFullYear() === today.getFullYear()
);
if (isToday) {
dayCell.classList.add('bg-primary/10', 'border-primary');
}
// 添加日期
const dateHeader = document.createElement('div');
dateHeader.className = 'day-number mb-1';
dateHeader.textContent = day;
dayCell.appendChild(dateHeader);
// 添加事件
if (eventsByDate[dateStr]) {
const eventsContainer = document.createElement('div');
eventsContainer.className = 'event-list space-y-1';
eventsByDate[dateStr].forEach(event => {
const eventElement = document.createElement('div');
eventElement.className = 'event-item p-1 text-sm rounded cursor-pointer hover:bg-gray-100 truncate';
eventElement.textContent = event.title;
eventElement.title = event.title; // 添加悬停提示
eventElement.onclick = () => showEventDetails(event);
eventsContainer.appendChild(eventElement);
});
dayCell.appendChild(eventsContainer);
}
calendarGrid.appendChild(dayCell);
}
calendar.appendChild(calendarGrid);
}
// 上个月
function prevMonth() {
currentDate.setMonth(currentDate.getMonth() - 1);
fetchMonthData(currentDate);
}
// 下个月
function nextMonth() {
currentDate.setMonth(currentDate.getMonth() + 1);
fetchMonthData(currentDate);
}
// 回到今天
function goToToday() {
currentDate = new Date(today.getFullYear(), today.getMonth(), today.getDate());
fetchMonthData(currentDate);
}
// 显示事件详情
function showEventDetails(event) {
const modal = document.getElementById('eventModal');
const modalContent = document.getElementById('eventModalContent');
if (!modal || !modalContent) {
console.error('Modal elements not found');
return;
}
modalContent.innerHTML = `
<h3 class="text-xl font-bold mb-4">${event.title}</h3>
<p class="mb-2">时间:${event.date} ${event.time}</p>
<p class="mb-4">${event.description || '无描述'}</p>
<div class="flex justify-end space-x-2">
<button onclick="editEvent(${event.id})" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">编辑</button>
<button onclick="deleteEvent(${event.id})" class="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600">删除</button>
<button onclick="closeEventModal()" class="px-4 py-2 bg-gray-500 text-white rounded hover:bg-gray-600">关闭</button>
</div>
`;
modal.classList.remove('hidden');
}
// 关闭事件详情模态框
function closeEventModal() {
const modal = document.getElementById('eventModal');
if (modal) {
modal.classList.add('hidden');
}
}
// 添加新事件
function addEvent() {
const titleInput = document.getElementById('eventTitle');
const dateInput = document.getElementById('eventDate');
const timeInput = document.getElementById('eventTime');
const descriptionInput = document.getElementById('eventDescription');
const title = titleInput.value.trim();
const date = dateInput.value;
const time = timeInput.value;
const description = descriptionInput.value.trim();
if (!title || !date || !time) {
alert('请填写事件名称、日期和时间');
return;
}
fetch('/Schedule/events/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken')
},
body: JSON.stringify({
title: title,
date: date,
time: time,
description: description
})
})
.then(response => {
if (!response.ok) {
throw new Error('添加事件失败: ' + response.status);
}
return response.json();
})
.then(data => {
// 刷新事件列表
fetchMonthData(currentDate);
// 清空表单
titleInput.value = '';
dateInput.value = '';
timeInput.value = '';
descriptionInput.value = '';
// 关闭模态框
closeAddEventModal();
// 显示成功提示
showNotification('日程添加成功');
})
.catch(error => {
console.error('添加事件失败:', error);
alert('添加事件失败: ' + error.message);
});
}
// 删除事件
function deleteEvent(eventId) {
if (confirm('确定要删除这个事件吗?')) {
fetch(`/Schedule/events/${eventId}/`, {
method: 'DELETE',
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
})
.then(response => {
if (!response.ok) {
throw new Error('删除事件失败');
}
closeEventModal();
fetchMonthData(currentDate);
showNotification('事件已删除');
})
.catch(error => {
console.error('Error deleting event:', error);
alert('删除事件失败');
});
}
}
// 初始化
document.addEventListener('DOMContentLoaded', () => {
fetchMonthData(currentDate);
fetchAndRenderTasks();
// 绑定事件监听
document.getElementById('addTaskBtn').addEventListener('click', function() {
const input = document.getElementById('newTaskInput');
const text = input.value.trim();
if (!text) {
alert('请输入任务内容');
return;
}
// 如果没有选择日期,默认使用今天
const eventDate = document.getElementById('eventDate').value;
const date = eventDate || formatDate(today);
// 先创建事件,然后添加任务
fetch('/Schedule/events/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken')
},
credentials: 'include',
body: JSON.stringify({
title: '待办事项',
date: date,
time: '00:00',
description: '自动创建的待办事项'
})
})
.then(response => {
if (!response.ok) {
throw new Error('创建事件失败: ' + response.status);
}
return response.json();
})
.then(eventData => {
// 创建任务
return fetch('/Schedule/tasks/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken')
},
credentials: 'include',
body: JSON.stringify({
text: text,
event: eventData.id
})
});
})
.then(response => {
if (!response.ok) {
throw new Error('添加任务失败: ' + response.status);
}
return response.json();
})
.then(() => {
// 清空输入框
input.value = '';
// 刷新任务列表
fetchAndRenderTasks();
// 刷新日历
fetchMonthData(currentDate);
showNotification('任务添加成功');
})
.catch(error => {
console.error('Error:', error);
alert('操作失败: ' + error.message);
});
});
document.getElementById('newTaskInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
document.getElementById('addTaskBtn').click();
}
});
document.getElementById('submitEventBtn').addEventListener('click', addEvent);
});
// 从API获取并渲染任务
function fetchAndRenderTasks(date) {
const targetDate = date || formatDate(today);
fetch(`/Schedule/tasks/?date=${targetDate}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken')
},
credentials: 'include'
})
.then(response => {
if (response.status === 401) {
throw new Error('请先登录');
}
if (!response.ok) {
throw new Error('获取任务数据失败: ' + response.status);
}
return response.json();
})
.then(data => {
window.tasks = data; // 保存到全局变量
renderTaskList();
})
.catch(error => {
console.error('Error fetching tasks:', error);
const taskList = document.getElementById('taskList');
if (error.message === '请先登录') {
taskList.innerHTML = `
<div class="text-center py-8 text-red-500">
<i class="fa fa-exclamation-triangle text-4xl mb-3"></i>
<p>请先登录</p>
<p class="text-sm mt-2">您需要登录才能查看待办事项</p>
<a href="{% url 'login' %}" class="inline-block mt-4 px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-light transition-colors">
去登录
</a>
</div>
`;
} else {
taskList.innerHTML = `
<div class="text-center py-8 text-red-500">
<i class="fa fa-exclamation-triangle text-4xl mb-3"></i>
<p>加载任务数据失败</p>
<p class="text-sm mt-2">${error.message}</p>
</div>
`;
}
});
}
// 渲染任务列表
function renderTaskList() {
const taskList = document.getElementById('taskList');
const taskCount = document.getElementById('taskCount');
// 更新任务计数
const incompleteTasks = window.tasks.filter(task => !task.completed);
taskCount.textContent = incompleteTasks.length;
// 清空任务列表
taskList.innerHTML = '';
// 如果没有任务,显示空状态
if (window.tasks.length === 0) {
taskList.innerHTML = `
<div class="text-center py-8 text-gray-400">
<i class="fa fa-check-circle text-4xl mb-3"></i>
<p>暂无待办事项</p>
</div>
`;
return;
}
// 渲染每个任务
window.tasks.forEach(task => {
const taskElement = document.createElement('div');
taskElement.className = `flex items-center p-3 rounded-lg border border-light ${task.completed ? 'bg-gray-50' : 'bg-white'} transition-all`;
taskElement.innerHTML = `
<div class="flex items-center">
<input type="checkbox" id="task-${task.id}" class="mr-3 h-5 w-5 text-primary rounded border-gray-300 focus:ring-primary" ${task.completed ? 'checked' : ''}>
<label for="task-${task.id}" class="flex-1 ${task.completed ? 'text-gray-500 line-through' : ''}">${task.text}</label>
</div>
<button class="delete-task-btn text-gray-400 hover:text-red-500 transition-colors ml-2" data-id="${task.id}">
<i class="fa fa-trash"></i>
</button>
`;
// 添加事件监听
taskElement.querySelector(`#task-${task.id}`).addEventListener('change', function() {
toggleTask(task.id);
});
taskElement.querySelector('.delete-task-btn').addEventListener('click', function() {
deleteTask(task.id);
});
taskList.appendChild(taskElement);
});
}
// 切换任务完成状态
function toggleTask(id) {
fetch(`/Schedule/tasks/${id}/toggle/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken')
},
credentials: 'include'
})
.then(response => {
if (!response.ok) {
throw new Error('更新任务状态失败: ' + response.status);
}
return response.json();
})
.then(data => {
// 更新本地任务状态
const taskIndex = window.tasks.findIndex(task => task.id === id);
if (taskIndex !== -1) {
window.tasks[taskIndex].completed = data.completed;
renderTaskList();
}
})
.catch(error => {
console.error('Error toggling task:', error);
alert('更新任务状态失败: ' + error.message);
});
}
// 删除任务
function deleteTask(id) {
if (confirm('确定要删除这个任务吗?')) {
fetch(`/Schedule/tasks/${id}/delete/`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken')
},
credentials: 'include'
})
.then(response => {
if (!response.ok) {
throw new Error('删除任务失败: ' + response.status);
}
// 刷新任务列表
fetchAndRenderTasks();
// 刷新事件列表
fetchMonthData(currentDate);
showNotification('任务已删除');
})
.catch(error => {
console.error('Error deleting task:', error);
alert('删除任务失败: ' + error.message);
});
}
}
</script>
</body>
</html>

+ 469
- 0
templates/chat.html View File

@ -0,0 +1,469 @@
<!doctypehtml>
<html lang=zh-CN>
<meta charset=UTF-8>
<meta content=width= device-width,initial-scale=1.0 name=viewport>
<title>聊天页面</title>
<script src=https://cdn.tailwindcss.com/3.4.16></script>
<script>
tailwind.config = {
darkMode: `class`
}
</script>
<link href=https://cdn.bootcdn.net/ajax/libs/font-awesome/6.4.0/css/all.min.css rel=stylesheet>
<style>
@import "https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&display=swap";:root {
--primary-purple: #6a1b9a;
--secondary-purple: #9c27b0;
--light-purple: #d1c4e9;
--dark-purple: #4a148c;
--background: #f3e5f5;
--text-primary: #333;
--text-secondary: #666;
--card-bg: #fff;
--card-border: #eee
}
.dark {
--primary-purple: #9c27b0;
--secondary-purple: #6a1b9a;
--light-purple: #4a148c;
--dark-purple: #6a1b9a;
--background: #121212;
--text-primary: #fff;
--text-secondary: #ccc;
--card-bg: #1e1e1e;
--card-border: #2a2a2a
}
body {
background-color: var(--background);
color: var(--text-primary);
font-family: Noto Sans SC,sans-serif;
transition: all .3s
}
.chat-container {
height: 100vh;
display: flex
}
.contacts-sidebar {
background-color: var(--primary-purple);
color: #fff;
width: 250px;
transition: all .3s
}
.chat-main {
flex: 1;
overflow-y: auto
}
.profile-sidebar {
background-color: var(--card-bg);
border-left: 1px solid var(--card-border);
width: 250px
}
.message {
border-radius: 8px;
margin-bottom: 15px;
padding: 12px;
transition: all .3s
}
.message.sent {
background-color: var(--light-purple);
align-self: flex-end
}
.message.received {
background-color: #f5f5f5;
align-self: flex-start
}
.message img,.message video {
border-radius: 8px;
max-width: 100%;
height: auto
}
.emoji-picker {
background-color: var(--card-bg);
border-radius: 8px;
box-shadow: 0 4px 6px #0000001a;
max-height: 200px;
padding: 10px;
overflow-y: auto
}
.emoji-category {
margin-bottom: 10px
}
.emoji-category-title {
color: var(--text-secondary);
margin-bottom: 5px;
font-size: 14px
}
.emoji-grid {
grid-template-columns: repeat(7,1fr);
gap: 5px;
display: grid
}
.emoji-item {
cursor: pointer;
font-size: 20px;
transition: transform .2s
}
.emoji-item:hover {
transform: scale(1.2)
}
.upload-button {
cursor: pointer;
background-color: var(--light-purple);
color: var(--primary-purple);
border-radius: 50%;
justify-content: center;
align-items: center;
width: 36px;
height: 36px;
margin-right: 10px;
font-size: 18px;
transition: all .3s;
display: flex
}
.upload-button:hover {
background-color: var(--secondary-purple);
color: #fff
}
.contact {
border-radius: 8px;
padding: 12px;
transition: all .3s
}
.contact:hover {
background-color: var(--secondary-purple)
}
.card {
background-color: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 8px;
margin-bottom: 15px;
padding: 15px;
transition: all .3s
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 6px #0000001a
}
.btn {
background-color: var(--primary-purple);
color: #fff;
border-radius: 4px;
padding: 8px 16px;
font-weight: 500;
transition: all .3s
}
.btn:hover {
background-color: var(--secondary-purple);
transform: scale(1.05)
}
.fade-in {
animation: .5s ease-in fadeIn
}
@keyframes fadeIn {
0% {
opacity: 0;
transform: translateY(10px)
}
to {
opacity: 1;
transform: translateY(0)
}
}
::-webkit-scrollbar {
width: 8px
}
::-webkit-scrollbar-track {
background: var(--background)
}
::-webkit-scrollbar-thumb {
background: var(--secondary-purple);
border-radius: 4px
}
::-webkit-scrollbar-thumb:hover {
background: var(--primary-purple)
}
</style>
<body class=min-h-screen>
<header class="py-6 px-4 md:px-8 text-center">
<a class="text-3xl md:text-4xl font-bold mb-4 fade-in" href=主页面.html>WaveSign手语通</a>
<p class="text-lg md:text-xl opacity-80 fade-in mt-4">连接双手与世界,构建无声的桥梁
</header>
<div class="min-h-screen flex flex-col">
<nav class="bg-white dark:bg-gray-800 shadow-md sticky top-0 z-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-center h-16">
<div class="flex items-center">
<div class="container mx-auto flex justify-center space-x-8">
<a class="nav-link text-lg font-medium" href=SLClassroom.html style=color:var(--text-color)>手语教室</a>
<a class="nav-link text-lg font-medium" href=LifeServing.html style=color:var(--text-color)>生活服务</a>
<a class="nav-link text-lg font-medium" href=Community.html style=color:var(--text-color)>手语社区</a>
<a class="nav-link text-lg font-medium" href=聊天页面.html style=color:var(--text-color)>聊天</a>
<a class="nav-link text-lg font-medium" href=myPage.html style=color:var(--text-color)>我的</a>
<button class="p-2 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700 focus:outline-none" id=theme-toggle>
<i class="fas fa-moon dark:hidden"></i>
<i class="fas fa-sun hidden dark:block text-yellow-300"></i>
</button>
</div>
</div>
</div>
</div>
</nav>
<div>
<div class="chat-container flex">
<div class="contacts-sidebar shadow-md">
<h2 class="text-xl font-bold p-4 border-b border-opacity-30 border-white">联系人</h2>
<ul class="p-2 space-y-2">
<li class="contact flex items-center p-2 cursor-pointer" onclick=switchChat(1)>
<img alt="张军的头像" class="w-10 h-10 rounded-full mr-3" src=images\OIP-C.jpg>
<span>张军</span>
<li class="contact flex items-center p-2 cursor-pointer" onclick=switchChat(2)>
<img alt="王暖暖的头像" class="w-10 h-10 rounded-full mr-3" src=images\聊天2.jpg>
<span>王暖暖</span>
</ul>
</div>
<div class="chat-main p-4">
<div class="chat-header bg-opacity-20 bg-white p-3 rounded-lg mb-4">
<div class="flex items-center">
<img alt=聊天对象头像 class="w-12 h-12 rounded-full mr-3" src=images\OIP-C.jpg id=chatAvatar>
<div>
<h3 class="font-bold text-lg" id=chatName>张军</h3>
<p class="text-sm opacity-75" id=chatStatus>在线
</div>
</div>
</div>
<div class="messages p-3" id=messages>
<div class="message received">
<p>你好!今天天气不错啊。
</div>
<div class="message sent">
<p>是啊,准备出去走走。
</div>
<div class="message received">
<p>有什么计划吗?
</div>
<div class="message sent">
<p>打算去公园散步,拍点照片。
</div>
<div class="message received">
<p>好的,我也去公园看看。
</div>
</div>
<div class="input-area flex items-center mt-4">
<input class="flex-1 px-4 py-2 rounded-l-lg focus:outline-none focus:ring-2 focus:ring-primary-purple" placeholder=输入消息... id=input type=text>
<label class=upload-button for=imageUpload>
<i class="fas fa-image"></i>
</label>
<input accept="image/*" id=imageUpload style=display:none type=file>
<label class=upload-button for=videoUpload>
<i class="fas fa-video"></i>
</label>
<input accept="video/*" id=videoUpload style=display:none type=file>
<button class=btn onclick=sendMessage()>
<i class="fas fa-paper-plane mr-1"></i>
发送
</button>
<button class="btn ml-2" onclick=showEmojis()>
<i class="far fa-smile mr-1"></i>
</button>
</div>
</div>
<div class="profile-sidebar shadow-md">
<div class="flex justify-center mb-4">
<img alt=个人资料头像 class="w-20 h-20 rounded-full border-4 border-white" src=images\OIP-C.jpg id=profileAvatar>
</div>
<h3 class="text-center text-xl font-bold mb-2" id=profileName>张军</h3>
<div class="card p-3 mb-4">
<p class="text-sm mb-1">
<i class="fas fa-venus-mars mr-2 text-primary-purple"></i>
性别:<span id=profileGender></span>
<p class="text-sm mb-1">
<i class="fas fa-map-marker-alt mr-2 text-primary-purple"></i>
属地:<span id=profileLocation>北京</span>
<p class="text-sm mb-1">
<i class="fas fa-quote-left mr-2 text-primary-purple"></i>
个性签名:<span id=profileBio>Hello World!</span>
</div>
<div class="emoji-picker fixed bottom-16 right-16 z-50" id=emojiPicker>
<div class=emoji-category>
<div class=emoji-category-title>表情符号</div>
<div class=emoji-grid>
<span class=emoji-item>😊</span>
<span class=emoji-item>😂</span>
<span class=emoji-item>🤔</span>
<span class=emoji-item>😢</span>
<span class=emoji-item>😠</span>
<span class=emoji-item>😲</span>
<span class=emoji-item>😄</span>
<span class=emoji-item>😊</span>
<span class=emoji-item>😂</span>
<span class=emoji-item>🤔</span>
<span class=emoji-item>😢</span>
<span class=emoji-item>😠</span>
<span class=emoji-item>😲</span>
<span class=emoji-item>😄</span>
<span class=emoji-item>😊</span>
<span class=emoji-item>😂</span>
<span class=emoji-item>🤔</span>
<span class=emoji-item>😢</span>
<span class=emoji-item>😠</span>
<span class=emoji-item>😲</span>
<span class=emoji-item>😄</span>
</div>
</div>
</div>
</div>
</div>
<script>
var contacts = [{
id: 1,
name: `张军`,
avatar: `images/OIP-C.jpg`,
gender: `男`,
location: `北京`,
bio: `Hello World!`
}, {
id: 2,
name: `王暖暖`,
avatar: `images/聊天2.jpg`,
gender: `女`,
location: `上海`,
bio: `Nice to meet you!`
}];
var currentContactId = 1;
var switchChat = (a => {
currentContactId = a;
document.getElementById(`messages`).innerHTML = ``;
const b = contacts.find(c => c.id === a);
document.getElementById(`chatAvatar`).src = b.avatar;
document.getElementById(`chatName`).innerText = b.name;
document.getElementById(`profileAvatar`).src = b.avatar;
document.getElementById(`profileName`).innerText = b.name;
document.getElementById(`profileGender`).innerText = ` ${b.gender}`;
document.getElementById(`profileLocation`).innerText = ` ${b.location}`;
document.getElementById(`profileBio`).innerText = ` ${b.bio}`
}
);
var sendMessage = ( () => {
const a = document.getElementById(`input`).value.trim();
if (a && currentContactId) {
const b = document.createElement(`div`);
b.className = `message sent`;
b.innerHTML = a;
document.getElementById(`messages`).appendChild(b);
document.getElementById(`input`).value = ``;
document.getElementById(`messages`).scrollTop = document.getElementById(`messages`).scrollHeight
}
}
);
var uploadImage = (a => {
const b = a.target.files[0];
if (b) {
const c = new FileReader();
c.onload = (a => {
const d = a.target.result;
sendImage(d)
}
);
c.readAsDataURL(b)
}
}
);
var uploadVideo = (a => {
const b = a.target.files[0];
if (b) {
const c = new FileReader();
c.onload = (a => {
const d = a.target.result;
sendVideo(d)
}
);
c.readAsDataURL(b)
}
}
);
var sendImage = (a => {
if (currentContactId) {
const b = document.createElement(`div`);
b.className = `message sent`;
const c = document.createElement(`img`);
c.src = a;
c.style.maxWidth = `200px`;
c.style.borderRadius = `8px`;
b.appendChild(c);
document.getElementById(`messages`).appendChild(b);
document.getElementById(`messages`).scrollTop = document.getElementById(`messages`).scrollHeight
}
}
);
var sendVideo = (a => {
if (currentContactId) {
const b = document.createElement(`div`);
b.className = `message sent`;
const c = document.createElement(`video`);
c.src = a;
c.controls = !0;
c.style.maxWidth = `200px`;
c.style.borderRadius = `8px`;
b.appendChild(c);
document.getElementById(`messages`).appendChild(b);
document.getElementById(`messages`).scrollTop = document.getElementById(`messages`).scrollHeight
}
}
);
var showEmojis = ( () => {
const a = document.getElementById(`emojiPicker`);
a.style.display = a.style.display === `none` ? `block` : `none`
}
);
document.querySelectorAll(`.emoji-item`).forEach(a => {
a.addEventListener(`click`, () => {
const b = a.textContent;
document.getElementById(`input`).value += b;
document.getElementById(`emojiPicker`).style.display = `none`
}
)
}
);
document.addEventListener(`DOMContentLoaded`, ( () => {
const a = document.querySelectorAll(`.message`);
a.forEach( (a, b) => {
setTimeout( () => {
a.classList.add(`fade-in`)
}
, b * 100)
}
)
}
))
</script>

+ 442
- 0
templates/myPage.html View File

@ -0,0 +1,442 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>我的</title>
<link rel="stylesheet" href="{% static 'css/myPage.css' %}">
<link rel="stylesheet" href="{% static 'css/login_button.css' %}">
<script src="{% static 'js/myPage.js' %}"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" rel="stylesheet">
<script src=https://cdn.tailwindcss.com></script>
</head>
<body style="background-image: url('{{ profile.page_background.url }}'); background-size: cover;">
<!-- 导航栏 -->
<header class="py-6 px-4 md:px-8 flex justify-between items-center bg-white bg-opacity-70">
<div class="absolute left-1/2 transform -translate-x-1/2">
<a class="text-3xl md:text-4xl font-bold fade-in">WaveSign手语通</a>
<p class="text-lg md:text-xl opacity-80 fade-in mt-2">连接双手与世界,构建无声的桥梁</p>
</div>
<div class="flex items-center space-x-4 ml-auto">
{% if user.is_authenticated %}
<div class="user-menu-box-container">
<a href="{% url 'my_page' %}" class="user-menu-box-button">
{% if user.avatar %}
<img src="{{ user.avatar.url }}" alt="头像" class="user-menu-box-avatar">
{% else %}
<img src="{% static 'images/default_avatar.png' %}" alt="默认头像" class="user-menu-box-avatar">
{% endif %}
{{ user.username }}
</a>
<div class="user-menu-box-dropdown">
<a href="{% url 'my_page' %}">进入个人主页</a>
<a href="{% url 'logout' %}">退出登录</a>
</div>
</div>
{% else %}
<a href="{% url 'login' %}" class="user-menu-box-login">
<i class="fas fa-user"></i> 登录
</a>
{% endif %}
</div>
</header>
<nav class="sticky top-0 bg-white bg-opacity-90 backdrop-blur-sm py-4 px-4 md:px-8 z-50 fade-in"
style="border-bottom: 1px solid var(--border-color);">
<div class="container mx-auto flex justify-center space-x-8">
<a class="nav-link text-lg font-medium" href="{% url 'home' %}" style="color:var(--text-color)">首页</a>
<a class="nav-link text-lg font-medium" href="{% url 'sl_classroom' %}"
style="color:var(--text-color)">手语教室</a>
<a class="nav-link text-lg font-medium" href="{% url 'life_serving' %}"
style="color:var(--text-color)">生活服务</a>
<a class="nav-link text-lg font-medium" href="{% url 'community' %}"
style="color:var(--text-color)">手语社区</a>
<a class="nav-link text-lg font-medium" href="{% url 'schedule' %}" style="color:var(--text-color)">日程管理</a>
<a class="nav-link text-lg font-medium" href="{% url 'my_page' %}" style="color:var(--text-color)">我的</a>
</div>
</nav>
<form method="POST" enctype="multipart/form-data">
{% csrf_token %}
<!-- 页面背景上传控件 -->
<div class="bg-upload-container">
<label for="bgInput" class="bg-upload-label">更换背景</label>
<input type="file" name="page_background" id="bgInput" class="bg-upload" accept="image/*"
onchange="changePageBg(event)">
</div>
<div class="profile-page">
<!-- 个人信息区域 -->
<div class="profile-page">
<div class="profile-header">
<div class="profile-container">
<!-- 个人资料背景 -->
<div class="profile-banner" id="profileBanner"
style="background-image: url('{{ profile.personal_background.url }}');">
<input type="file" name="personal_background" accept="image/*" hidden id="uploadBanner"
onchange="changeBanner(event)">
<button type="button" class="change-banner-btn"
onclick="document.getElementById('uploadBanner').click()">更换个人资料背景</button>
</div>
<!-- 头像 -->
<div class="profile-avatar-container">
<input type="file" name="avatar" accept="image/*" hidden id="uploadAvatar"
onchange="changeAvatar(event)">
<img src="{% if user.avatar %}{{ user.avatar.url }}{% else %}{% static 'images/default_avatar.png' %}{% endif %}"
class="profile-avatar" id="profileAvatar"
onclick="document.getElementById('uploadAvatar').click()">
</div>
<!-- 用户资料 -->
<div class="profile-content">
<h2 class="profile-name">{{ user.username }}</h2>
<p class="profile-bio">{{ profile.bio }}</p>
<!-- 编辑弹窗 -->
<div class="modal-overlay" id="modalOverlay"></div>
<div class="profile-modal" id="profileModal">
<span class="close-modal" onclick="closeProfileEdit()">&times;</span>
<h3 style="text-align:center;">编辑个人资料</h3>
<label>用户名:</label>
<input type="text" name="username" value="{{ user.username }}">
<label>个性签名:</label>
<textarea name="bio">{{ profile.bio }}</textarea>
<button type="submit" class="update-profile-btn">更新资料</button>
</div>
<div class="profile-update-notice" id="profileUpdateNotice">个人资料已更新!</div>
<button class="edit-profile-btn" type="button" onclick="openProfileEdit()">编辑资料</button>
</div>
</div>
</div>
</div>
</div>
</form>
<!-- 分栏布局 -->
<div class="main-content">
<!-- 左栏:关注列表 -->
<div class="left-column">
<h3>我关注的人</h3>
<hr class="divider">
<div class="friend-list-container">
{% for follow in follows %}
<div class="friend-item">
<img class="w-10 h-10 rounded-full object-cover"
src="{{ follow.followed.avatar.url|default:'/static/images/default_avatar.png' }}" alt="头像"
class="user-avatar" />
<span>{{ follow.followed.username }}</span>
</div>
{% empty %}
<p class="text-gray-400">你还没有关注任何人</p>
{% endfor %}
</div>
</div>
<!-- 中栏:帖子内容 -->
<div class="middle-column">
<div class="tab">
<button class="tablinks" onclick="openTab(event, 'myPosts')" id="defaultOpen">
<i class="fas fa-home"></i> 我的帖子
</button>
<button class="tablinks" onclick="openTab(event, 'myFavorites')">
<i class="fas fa-heart"></i> 我的收藏
</button>
<button class="tablinks" onclick="openTab(event, 'myLikes')">
<i class="fas fa-thumbs-up"></i> 我的赞
</button>
</div>
<!-- 我的帖子 -->
<div id="myPosts" class="tabcontent">
{% for post in my_posts %}
<div class="relative post-card shadow-md rounded-lg p-4 mb-6 bg-white dark:bg-gray-800">
<!-- 用户信息区 -->
<div class="post-header flex items-center space-x-3 mb-2">
{% if post.user.avatar %}
<img class="w-10 h-10 rounded-full object-cover" src="{{ post.user.avatar.url }}" alt="用户头像">
{% else %}
<img class="w-10 h-10 rounded-full object-cover" src="{% static 'images/default_avatar.png' %}"
alt="默认头像">
{% endif %}
<div>
<h3 class="font-semibold text-gray-900 dark:text-gray-100">{{ post.user.username }}</h3>
<p class="text-sm text-gray-500">{{ post.created_at|timesince }}前</p>
</div>
<div class="absolute top-4 right-4 z-10">
<button onclick="toggleOptionsMenuDelete(this)"
class="text-gray-500 hover:text-gray-800 dark:hover:text-white">
<i class="fas fa-ellipsis-h"></i>
</button>
<div
class="options-menu hidden absolute right-0 mt-2 w-32 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-lg">
<form method="post" action="{% url 'delete_post' post.id %}"
onsubmit="return confirm('确定要删除这条帖子吗?');">
{% csrf_token %}
<button type="submit"
class="block w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-gray-100 dark:hover:bg-gray-600">
删除帖子
</button>
</form>
</div>
</div>
</div>
<!-- 帖子内容 -->
<div class="post-content text-gray-800 dark:text-gray-200 mb-2">
<p>{{ post.content }}</p>
</div>
<!-- 媒体展示 -->
<div class="post-media space-y-2">
{% if post.image %}
<img src="{{ post.image.url }}" alt="帖子图片" class="w-full rounded-lg">
{% endif %}
{% if post.video %}
<video controls class="w-1/2 h-auto rounded-lg">
<source src="{{ post.video.url }}" type="video/mp4">
</video>
{% endif %}
</div>
<!-- 简洁信息展示区 -->
<div class="post-actions flex space-x-4 text-sm text-gray-600 mt-4">
<div><i class="fa-regular fa-heart mr-1"></i>{{ post.like_count }}</div>
<div><i class="fa-regular fa-comment-dots mr-1"></i>{{ post.comment_count }}</div>
<div><i class="fa-regular fa-bookmark mr-1"></i>{{ post.favorite_count }}</div>
</div>
<!-- 评论展示(最多展示前几条) -->
{% if post.comment_set.all %}
<div class="post-comments mt-4 space-y-2 text-sm text-gray-700 dark:text-gray-300">
{% for comment in post.comment_set.all|slice:":3" %}
<div><strong>{{ comment.user.username }}</strong>: {{ comment.content }}</div>
{% endfor %}
{% if post.comment_set.count > 3 %}
<div class="text-gray-400 italic">……更多评论</div>
{% endif %}
</div>
{% endif %}
</div>
{% empty %}
<p class="text-gray-500 text-center mt-6">暂无内容。</p>
{% endfor %}
</div>
<!-- 我的收藏 -->
<div id="myFavorites" class="tabcontent" style="display: none;">
{% for post in favorited_posts %}
<div class="post-card shadow-md rounded-lg p-4 mb-6 bg-white dark:bg-gray-800">
<!-- 用户信息区 -->
<div class="post-header flex items-center space-x-3 mb-2">
{% if post.user.avatar %}
<img class="w-10 h-10 rounded-full object-cover" src="{{ post.user.avatar.url }}" alt="用户头像">
{% else %}
<img class="w-10 h-10 rounded-full object-cover" src="{% static 'images/default_avatar.png' %}"
alt="默认头像">
{% endif %}
<div>
<h3 class="font-semibold text-gray-900 dark:text-gray-100">{{ post.user.username }}</h3>
<p class="text-sm text-gray-500">{{ post.created_at|timesince }}前</p>
</div>
</div>
<!-- 帖子内容 -->
<div class="post-content text-gray-800 dark:text-gray-200 mb-2">
<p>{{ post.content }}</p>
</div>
<!-- 媒体展示 -->
<div class="post-media space-y-2">
{% if post.image %}
<img src="{{ post.image.url }}" alt="帖子图片" class="w-full rounded-lg">
{% endif %}
{% if post.video %}
<video controls class="w-1/2 h-auto rounded-lg">
<source src="{{ post.video.url }}" type="video/mp4">
</video>
{% endif %}
</div>
<!-- 简洁信息展示区 -->
<div class="post-actions flex space-x-4 text-sm text-gray-600 mt-4">
<div><i class="fa-regular fa-heart mr-1"></i>{{ post.like_count }}</div>
<div><i class="fa-regular fa-comment-dots mr-1"></i>{{ post.comment_count }}</div>
<div><i class="fa-regular fa-bookmark mr-1"></i>{{ post.favorite_count }}</div>
</div>
<!-- 评论展示(最多展示前几条) -->
{% if post.comment_set.all %}
<div class="post-comments mt-4 space-y-2 text-sm text-gray-700 dark:text-gray-300">
{% for comment in post.comment_set.all|slice:":3" %}
<div><strong>{{ comment.user.username }}</strong>: {{ comment.content }}</div>
{% endfor %}
{% if post.comment_set.count > 3 %}
<div class="text-gray-400 italic">……更多评论</div>
{% endif %}
</div>
{% endif %}
</div>
{% empty %}
<p class="text-gray-500 text-center mt-6">暂无内容。</p>
{% endfor %}
</div>
<!-- 我的点赞 -->
<div id="myLikes" class="tabcontent" style="display: none;">
{% for post in liked_posts %}
<div class="post-card shadow-md rounded-lg p-4 mb-6 bg-white dark:bg-gray-800">
<!-- 用户信息区 -->
<div class="post-header flex items-center space-x-3 mb-2">
{% if post.user.avatar %}
<img class="w-10 h-10 rounded-full object-cover" src="{{ post.user.avatar.url }}" alt="用户头像">
{% else %}
<img class="w-10 h-10 rounded-full object-cover" src="{% static 'images/default_avatar.png' %}"
alt="默认头像">
{% endif %}
<div>
<h3 class="font-semibold text-gray-900 dark:text-gray-100">{{ post.user.username }}</h3>
<p class="text-sm text-gray-500">{{ post.created_at|timesince }}前</p>
</div>
</div>
<!-- 帖子内容 -->
<div class="post-content text-gray-800 dark:text-gray-200 mb-2">
<p>{{ post.content }}</p>
</div>
<!-- 媒体展示 -->
<div class="post-media space-y-2">
{% if post.image %}
<img src="{{ post.image.url }}" alt="帖子图片" class="w-full rounded-lg">
{% endif %}
{% if post.video %}
<video controls class="w-1/2 h-auto rounded-lg">
<source src="{{ post.video.url }}" type="video/mp4">
</video>
{% endif %}
</div>
<!-- 简洁信息展示区 -->
<div class="post-actions flex space-x-4 text-sm text-gray-600 mt-4">
<div><i class="fa-regular fa-heart mr-1"></i>{{ post.like_count }}</div>
<div><i class="fa-regular fa-comment-dots mr-1"></i>{{ post.comment_count }}</div>
<div><i class="fa-regular fa-bookmark mr-1"></i>{{ post.favorite_count }}</div>
</div>
<!-- 评论展示(最多展示前几条) -->
{% if post.comment_set.all %}
<div class="post-comments mt-4 space-y-2 text-sm text-gray-700 dark:text-gray-300">
{% for comment in post.comment_set.all|slice:":3" %}
<div><strong>{{ comment.user.username }}</strong>: {{ comment.content }}</div>
{% endfor %}
{% if post.comment_set.count > 3 %}
<div class="text-gray-400 italic">……更多评论</div>
{% endif %}
</div>
{% endif %}
</div>
{% empty %}
<p class="text-gray-500 text-center mt-6">暂无内容。</p>
{% endfor %}
</div>
</div>
<!-- 右栏:互动通知区 -->
<div class="right-column">
<!-- 📅 即将到来的日程 -->
<div class="notification-card mt-4">
<h4>📅 即将到来的日程</h4>
<ul>
{% for event in upcoming_events %}
<li class="text-sm mb-2">
<i class="fas fa-calendar-alt text-purple-500 mr-1"></i>
<strong>{{ event.title }}</strong><br>
<span class="text-gray-500 text-xs">{{ event.datetime|date:"Y年m月d日 H:i" }}</span>
</li>
{% endfor %}
{% if not upcoming_events %}
<li class="text-gray-500">暂无即将到来的日程</li>
{% endif %}
</ul>
<div class="text-right mt-2">
<a href="{% url 'schedule' %}" class="text-sm text-primary hover:underline">查看全部日程 &rarr;</a>
</div>
</div>
<!-- ✅ 我的待办事项 -->
<div class="notification-card mt-4">
<h4>✅ 我的待办事项</h4>
<ul>
{% for task in recent_tasks %}
<li class="text-sm mb-2">
<i class="fas fa-check-circle text-green-500 mr-1"></i>
{{ task.text }}
<span class="text-gray-400 text-xs block">添加于 {{ task.created_at|date:"m月d日 H:i" }}</span>
</li>
{% endfor %}
{% if not recent_tasks %}
<li class="text-gray-500">暂无未完成的任务</li>
{% endif %}
</ul>
<div class="text-right mt-2">
<a href="{% url 'schedule' %}" class="text-sm text-primary hover:underline">前往日程管理 &rarr;</a>
</div>
</div>
<div class="notification-card notifications">
<h4>🔔 互动通知</h4>
<ul>
{% for like in recent_likes %}
<li>
<i class="fas fa-heart text-red-500"></i>
<strong>{{ like.user.username }}</strong> 点赞了你的帖子:
<span>{{ like.post.content|truncatechars:12 }}</span>
</li>
{% endfor %}
{% for comment in recent_comments %}
<li>
<i class="fas fa-comment-dots text-blue-500"></i>
<strong>{{ comment.user.username }}</strong> 评论了你的帖子:
<span>{{ comment.content|truncatechars:12 }}</span>
</li>
{% endfor %}
{% if not recent_likes and not recent_comments %}
<li class="text-gray-500">暂无互动</li>
{% endif %}
</ul>
</div>
</div>
</div>
<footer class="py-8 px-4 md:px-8 text-center fade-in" style=animation-delay:1.2s>
<div class="flex flex-col items-center">
<div class="mb-4 text-lg font-medium">作者: backpack</div>
<p class="text-sm opacity-70">© 2025 WaveSign.
</div>
</footer>
</body>
</html>

Loading…
Cancel
Save