Compare commits
1 Commits
main
...
6e902bcc13
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e902bcc13 |
32
README.md
@@ -1,32 +1,2 @@
|
||||
# Vue
|
||||
|
||||
<p align="center">
|
||||
<img src="https://github.com/Dichgrem/Vue/blob/main/todos/view.png" width="300">
|
||||
</p>
|
||||
|
||||
## 概述
|
||||
|
||||
这是一个基于 **Vue 3 + Pinia** 的轻量级待办事项 (Todo) 项目,具有以下功能:
|
||||
|
||||
* 添加/删除/编辑待办事项
|
||||
* 按照全部 / 未完成 / 已完成进行筛选
|
||||
* 拖拽排序待办事项
|
||||
* 一键清除已完成事项
|
||||
* 本地存储 (`localStorage`) 数据持久化
|
||||
* 控制台打印操作日志,便于调试
|
||||
|
||||
|
||||
## 结构
|
||||
|
||||
```
|
||||
.
|
||||
├── App.vue # 根组件,管理整体状态和方法
|
||||
├── main.js # 项目入口
|
||||
├── stores/todo.js # Pinia 状态管理,存储 todos 数据和操作方法
|
||||
├── components
|
||||
│ ├── TodoHeader.vue # 输入框和添加功能
|
||||
│ ├── TodoList.vue # 待办事项列表,支持编辑和拖拽排序
|
||||
│ └── TodoFooter.vue # 底部统计、筛选和清除已完成
|
||||
├── assets # 静态资源(CSS/图片)
|
||||
└── style.css # 全局样式
|
||||
```
|
||||
This is a Vue test on school.
|
||||
24
guess-game/.gitignore
vendored
@@ -1,24 +0,0 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
3
guess-game/.vscode/extensions.json
vendored
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
# Vue 3 + Vite
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
|
||||
@@ -1,13 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>guess-game</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
2532
guess-game/package-lock.json
generated
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"name": "guess-game",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.5.24"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"vite": "^7.2.4",
|
||||
"vite-plugin-vue-devtools": "^8.0.5"
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,131 +0,0 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<div class="header">
|
||||
<h1>Vue3 猜数游戏</h1>
|
||||
<div class="toggle-buttons">
|
||||
<button
|
||||
:class="{ active: currentView === 'composition' }"
|
||||
@click="currentView = 'composition'"
|
||||
>
|
||||
组合式 API
|
||||
</button>
|
||||
<button
|
||||
:class="{ active: currentView === 'options' }"
|
||||
@click="currentView = 'options'"
|
||||
>
|
||||
选项式 API
|
||||
</button>
|
||||
<button
|
||||
:class="{ active: currentView === 'both' }"
|
||||
@click="currentView = 'both'"
|
||||
>
|
||||
对比查看
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<!-- 单独显示组合式 -->
|
||||
<GuessComposition v-if="currentView === 'composition'" />
|
||||
|
||||
<!-- 单独显示选项式 -->
|
||||
<GuessOptions v-if="currentView === 'options'" />
|
||||
|
||||
<!-- 并排对比显示 -->
|
||||
<div v-if="currentView === 'both'" class="comparison">
|
||||
<GuessComposition />
|
||||
<GuessOptions />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import GuessComposition from "./components/GuessComposition.vue";
|
||||
import GuessOptions from "./components/GuessOptions.vue";
|
||||
|
||||
const currentView = ref("both"); // 默认显示对比视图
|
||||
</script>
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#app {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
color: white;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.5em;
|
||||
margin-bottom: 20px;
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.toggle-buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.toggle-buttons button {
|
||||
padding: 10px 20px;
|
||||
border: 2px solid white;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
border-radius: 25px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.toggle-buttons button:hover {
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.toggle-buttons button.active {
|
||||
background-color: white;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.content {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.comparison {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));
|
||||
gap: 20px;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.comparison {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 1.8em;
|
||||
}
|
||||
|
||||
.toggle-buttons button {
|
||||
font-size: 14px;
|
||||
padding: 8px 15px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 496 B |
@@ -1,160 +0,0 @@
|
||||
<template>
|
||||
<div class="guess-container">
|
||||
<h2>猜数游戏</h2>
|
||||
<strong>请输入一个 0-100 之间的随机整数:</strong>
|
||||
<br /><br />
|
||||
|
||||
<form @submit.prevent>
|
||||
<input
|
||||
v-model="guessInput"
|
||||
type="text"
|
||||
placeholder="输入你的猜测"
|
||||
:disabled="!isGameActive"
|
||||
/>
|
||||
<button type="button" @click="handleGuess" :disabled="!isGameActive">
|
||||
提交
|
||||
</button>
|
||||
<button type="button" @click="restartGame">开始</button>
|
||||
<br />
|
||||
|
||||
<label>
|
||||
结果:
|
||||
<input v-model="feedback" type="text" readonly />
|
||||
</label>
|
||||
|
||||
<label>
|
||||
当前还可以猜测的次数:
|
||||
<input v-model="remainingCount" type="text" readonly />
|
||||
</label>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
|
||||
// 响应式变量定义
|
||||
const guessInput = ref(""); // 用户输入的猜测值
|
||||
const feedback = ref(""); // 反馈信息
|
||||
const remainingCount = ref(10); // 剩余猜测次数
|
||||
const targetNumber = ref(0); // 目标随机数
|
||||
const isGameActive = ref(false); // 游戏是否进行中
|
||||
|
||||
// 重启游戏方法
|
||||
const restartGame = () => {
|
||||
isGameActive.value = true;
|
||||
guessInput.value = "";
|
||||
feedback.value = "";
|
||||
remainingCount.value = 10;
|
||||
targetNumber.value = Math.floor(Math.random() * 101); // 生成 0-100 的随机数
|
||||
console.log("目标数字:", targetNumber.value); // 调试用
|
||||
};
|
||||
|
||||
// 处理猜测逻辑
|
||||
const handleGuess = () => {
|
||||
if (!isGameActive.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const guess = parseInt(guessInput.value);
|
||||
|
||||
// 验证输入是否有效
|
||||
if (isNaN(guess) || guess < 0 || guess > 100) {
|
||||
feedback.value = "请输入一个 0-100 之间的有效数字!";
|
||||
return;
|
||||
}
|
||||
|
||||
// 判断猜测结果
|
||||
if (guess > targetNumber.value) {
|
||||
feedback.value = "猜的有点大!";
|
||||
} else if (guess < targetNumber.value) {
|
||||
feedback.value = "猜的有点小!";
|
||||
} else {
|
||||
feedback.value = `猜对啦!答案是 ${guess}`;
|
||||
isGameActive.value = false; // 猜对后结束游戏
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新剩余次数
|
||||
remainingCount.value -= 1;
|
||||
|
||||
// 检查游戏是否结束
|
||||
if (remainingCount.value === 0) {
|
||||
feedback.value = `游戏结束!次数已用完,正确答案是 ${targetNumber.value}`;
|
||||
isGameActive.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.guess-container {
|
||||
text-align: center;
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f0f8ff;
|
||||
padding: 40px;
|
||||
border-radius: 10px;
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
form {
|
||||
display: inline-block;
|
||||
padding: 20px;
|
||||
background-color: #fff;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
padding: 8px;
|
||||
margin-bottom: 10px;
|
||||
width: 150px;
|
||||
text-align: center;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #ccc;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
input[type="text"]:disabled {
|
||||
background-color: #f5f5f5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 8px 15px;
|
||||
margin: 5px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #007bff;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
button:hover:not(:disabled) {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background-color: #ccc;
|
||||
border-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin: 15px 0;
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
label input {
|
||||
margin-left: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,181 +0,0 @@
|
||||
<template>
|
||||
<div class="guess-container">
|
||||
<h2>猜数游戏</h2>
|
||||
<strong>请输入一个 0-100 之间的随机整数:</strong>
|
||||
<br /><br />
|
||||
|
||||
<form @submit.prevent>
|
||||
<input
|
||||
v-model="guessInput"
|
||||
type="text"
|
||||
placeholder="输入你的猜测"
|
||||
:disabled="!isGameActive"
|
||||
@keyup.enter="handleGuess"
|
||||
/>
|
||||
<button type="button" @click="handleGuess" :disabled="!isGameActive">
|
||||
提交
|
||||
</button>
|
||||
<button type="button" @click="restartGame">开始</button>
|
||||
<br />
|
||||
|
||||
<label>
|
||||
结果:
|
||||
<input v-model="feedback" type="text" readonly />
|
||||
</label>
|
||||
|
||||
<label>
|
||||
当前还可以猜测的次数:
|
||||
<input v-model="remainingCount" type="text" readonly />
|
||||
</label>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "GuessOptions",
|
||||
|
||||
// 数据选项
|
||||
data() {
|
||||
return {
|
||||
guessInput: "",
|
||||
feedback: "",
|
||||
remainingCount: 10,
|
||||
targetNumber: 0,
|
||||
isGameActive: false,
|
||||
};
|
||||
},
|
||||
|
||||
// 方法选项
|
||||
methods: {
|
||||
// 重启游戏
|
||||
restartGame() {
|
||||
this.isGameActive = true;
|
||||
this.guessInput = "";
|
||||
this.feedback = "";
|
||||
this.remainingCount = 10;
|
||||
this.targetNumber = Math.floor(Math.random() * 101);
|
||||
console.log("选项式 - 目标数字:", this.targetNumber);
|
||||
},
|
||||
|
||||
// 处理猜测
|
||||
handleGuess() {
|
||||
if (!this.isGameActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
const guess = parseInt(this.guessInput);
|
||||
|
||||
// 验证输入
|
||||
if (isNaN(guess) || guess < 0 || guess > 100) {
|
||||
this.feedback = "请输入一个 0-100 之间的有效数字!";
|
||||
return;
|
||||
}
|
||||
|
||||
// 判断结果
|
||||
if (guess > this.targetNumber) {
|
||||
this.feedback = "猜的有点大!";
|
||||
} else if (guess < this.targetNumber) {
|
||||
this.feedback = "猜的有点小!";
|
||||
} else {
|
||||
this.feedback = `猜对啦!答案是 ${guess}`;
|
||||
this.isGameActive = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新次数
|
||||
this.remainingCount -= 1;
|
||||
|
||||
// 检查游戏结束
|
||||
if (this.remainingCount === 0) {
|
||||
this.feedback = `游戏结束!次数已用完,正确答案是 ${this.targetNumber}`;
|
||||
this.isGameActive = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
// 计算属性
|
||||
computed: {
|
||||
canGuess() {
|
||||
return this.isGameActive && this.remainingCount > 0;
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
console.log("选项式组件已挂载");
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.guess-container {
|
||||
text-align: center;
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f0f8ff;
|
||||
padding: 40px;
|
||||
border-radius: 10px;
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
form {
|
||||
display: inline-block;
|
||||
padding: 20px;
|
||||
background-color: #fff;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
padding: 8px;
|
||||
margin-bottom: 10px;
|
||||
width: 150px;
|
||||
text-align: center;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #ccc;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
input[type="text"]:disabled {
|
||||
background-color: #f5f5f5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 8px 15px;
|
||||
margin: 5px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #007bff;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
button:hover:not(:disabled) {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background-color: #ccc;
|
||||
border-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin: 15px 0;
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
label input {
|
||||
margin-left: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +0,0 @@
|
||||
import { createApp } from "vue";
|
||||
import "/src/style.css";
|
||||
import App from "/src/App.vue";
|
||||
|
||||
createApp(App).mount("#app");
|
||||
@@ -1,11 +0,0 @@
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { defineConfig } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import VueDevTools from "vite-plugin-vue-devtools";
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
base: "./",
|
||||
plugins: [vue(), VueDevTools()],
|
||||
});
|
||||
6
package-lock.json
generated
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "Vue",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
24
student-vol/.gitignore
vendored
@@ -1,24 +0,0 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
3
student-vol/.vscode/extensions.json
vendored
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
# Vue 3 + Vite
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
|
||||
@@ -1,13 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>student-vol</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"error": 0,
|
||||
"data": {
|
||||
"details": {
|
||||
"id": 1,
|
||||
"title": "运动会志愿者招募",
|
||||
"startTime": 1671494400000,
|
||||
"endTime": 1671697800000,
|
||||
"place": "福州大学至诚学院",
|
||||
"content": "为确保运动会顺利进行,现招募志愿者协助现场秩序维护、运动员服务等工作。",
|
||||
"publisher": "福州大学至诚学院团委",
|
||||
"hour": 4,
|
||||
"total": 50,
|
||||
"canApply": false,
|
||||
"applyStatus": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"error": 0,
|
||||
"data": {
|
||||
"current": 1,
|
||||
"pageSize": 10,
|
||||
"pageCount": 1,
|
||||
"list": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "运动会志愿者招募",
|
||||
"startTime": 1671494400000,
|
||||
"endTime": 1671697800000,
|
||||
"place": "福州大学至诚学院",
|
||||
"isYourSchool": true,
|
||||
"canApply": false,
|
||||
"actPic": "./imgs/actImg.jpeg"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "图书馆志愿服务",
|
||||
"startTime": 1670284800000,
|
||||
"endTime": 1670371200000,
|
||||
"place": "福州市图书馆",
|
||||
"isYourSchool": false,
|
||||
"canApply": false,
|
||||
"actPic": "./imgs/actImg.jpeg"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title": "社区敬老院服务",
|
||||
"startTime": 1672012800000,
|
||||
"endTime": 1672099200000,
|
||||
"place": "福州市养老院",
|
||||
"isYourSchool": false,
|
||||
"canApply": false,
|
||||
"actPic": "./imgs/actImg.jpeg"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"error": 0,
|
||||
"data": {
|
||||
"list": [
|
||||
{
|
||||
"value": 2,
|
||||
"text": "2小时"
|
||||
},
|
||||
{
|
||||
"value": 4,
|
||||
"text": "4小时"
|
||||
},
|
||||
{
|
||||
"value": 6,
|
||||
"text": "6小时"
|
||||
},
|
||||
{
|
||||
"value": 8,
|
||||
"text": "8小时"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
{
|
||||
"error": 0,
|
||||
"data": {
|
||||
"current": 1,
|
||||
"pageSize": 10,
|
||||
"pageCount": 1,
|
||||
"list": [
|
||||
{
|
||||
"id": 3,
|
||||
"title": "社区敬老院服务",
|
||||
"startTime": 1672012800000,
|
||||
"endTime": 1672099200000,
|
||||
"place": "福州市养老院",
|
||||
"isYourSchool": false,
|
||||
"canApply": true,
|
||||
"actPic": "./imgs/actImg.jpeg",
|
||||
"applyStatus": 1
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"title": "运动会志愿者招募",
|
||||
"startTime": 1671494400000,
|
||||
"endTime": 1671697800000,
|
||||
"place": "福州大学至诚学院",
|
||||
"isYourSchool": true,
|
||||
"canApply": true,
|
||||
"actPic": "./imgs/actImg.jpeg",
|
||||
"applyStatus": 1
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "图书馆志愿服务",
|
||||
"startTime": 1670284800000,
|
||||
"endTime": 1670371200000,
|
||||
"place": "福州市图书馆",
|
||||
"isYourSchool": false,
|
||||
"canApply": false,
|
||||
"actPic": "./imgs/actImg.jpeg",
|
||||
"applyStatus": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"error": 0,
|
||||
"data": {
|
||||
"list": [
|
||||
{
|
||||
"value": 1,
|
||||
"text": "福州大学至诚学院团委"
|
||||
},
|
||||
{
|
||||
"value": 2,
|
||||
"text": "福州市志愿者协会"
|
||||
},
|
||||
{
|
||||
"value": 3,
|
||||
"text": "鼓楼区社区服务中心"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
{
|
||||
"error": 0,
|
||||
"data": {
|
||||
"list": [
|
||||
{
|
||||
"id": 1,
|
||||
"avatar": "./imgs/avatar.png",
|
||||
"name": "张三",
|
||||
"score": 256
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"avatar": "./imgs/avatar.png",
|
||||
"name": "李四",
|
||||
"score": 238
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"avatar": "./imgs/avatar.png",
|
||||
"name": "王五",
|
||||
"score": 215
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"avatar": "./imgs/avatar.png",
|
||||
"name": "赵六",
|
||||
"score": 198
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"avatar": "./imgs/avatar.png",
|
||||
"name": "孙七",
|
||||
"score": 176
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"error": 0,
|
||||
"data": {
|
||||
"details": {
|
||||
"actTime": 1671494400000,
|
||||
"place": "福州大学至诚学院",
|
||||
"content": "参与运动会志愿服务,协助现场秩序维护、运动员服务等工作。",
|
||||
"imgList": [
|
||||
"./imgs/actImg.jpeg",
|
||||
"./imgs/actImg.jpeg"
|
||||
],
|
||||
"hour": 4,
|
||||
"uploadTime": 1671580800000,
|
||||
"status": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"error": 0,
|
||||
"data": {
|
||||
"provinceRank": 156,
|
||||
"totalScore": 180,
|
||||
"grandeRank": 8
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"error": 0,
|
||||
"msg": "success"
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"error": 0,
|
||||
"data": {
|
||||
"avatar": "./imgs/avatar.png",
|
||||
"name": "dich",
|
||||
"code": 114514,
|
||||
"gender": 0,
|
||||
"phoneNum": "1919810",
|
||||
"school": "福州大学至诚学院",
|
||||
"profession": "计算机",
|
||||
"userType": 2,
|
||||
"totalScore": 180
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"error": 0,
|
||||
"data": {
|
||||
"avatar": "./imgs/avatar.png",
|
||||
"name": "dich",
|
||||
"totalScore": 180
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"error": 0,
|
||||
"data": {
|
||||
"list": [
|
||||
{
|
||||
"value": 2023,
|
||||
"text": "2023年"
|
||||
},
|
||||
{
|
||||
"value": 2024,
|
||||
"text": "2024年"
|
||||
},
|
||||
{
|
||||
"value": 2025,
|
||||
"text": "2025年"
|
||||
},
|
||||
{
|
||||
"value": 2026,
|
||||
"text": "2026年"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"error": 0,
|
||||
"data": {
|
||||
"times": 16,
|
||||
"duration": 74,
|
||||
"score": 180
|
||||
}
|
||||
}
|
||||
2879
student-vol/package-lock.json
generated
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"name": "student-vol",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.13.2",
|
||||
"moment": "^2.30.1",
|
||||
"vant": "^4.9.22",
|
||||
"vue": "^3.5.24",
|
||||
"vue-router": "^4.6.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"vite": "^7.2.4",
|
||||
"vite-plugin-vue-devtools": "^8.0.5",
|
||||
"vite-plugin-vue-inspector": "^5.3.2"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,27 +0,0 @@
|
||||
<template>
|
||||
<router-view/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "App",
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
#app > .container {
|
||||
min-height: 100vh;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
|
Before Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 340 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 502 B |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 785 B |
|
Before Width: | Height: | Size: 889 B |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 711 B |
|
Before Width: | Height: | Size: 486 B |
|
Before Width: | Height: | Size: 649 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 496 B |
@@ -1,106 +0,0 @@
|
||||
<template>
|
||||
<div class="actItem" @click="detailsClick(data.id)">
|
||||
<div class="actItem-pic">
|
||||
<img :src="data.actPic" />
|
||||
<span class="actItemStatus" :style="getTagStyle(data)">
|
||||
{{ getTagTxt(data) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="actItem-body">
|
||||
<p class="actItem-title">{{ data.title }}</p>
|
||||
<p>
|
||||
{{ formatDate(data.startTime) }} 至
|
||||
{{ formatDate(data.endTime) }}
|
||||
</p>
|
||||
<p class="actItem-tag">
|
||||
{{ data.isYourSchool ? "本人所在学校活动" : data.place }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import moment from "moment";
|
||||
|
||||
export default {
|
||||
props: ["data", "isMyApply"],
|
||||
data() {
|
||||
return {
|
||||
applyStatusMap: ["", "审核中", "审核通过", "审核拒绝"],
|
||||
bgColorMap: ["", "#1989fa", "#07c160", "#f6352c"],
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getTagStyle(data) {
|
||||
if (this.isMyApply && data.applyStatus !== undefined) {
|
||||
return { backgroundColor: this.bgColorMap[data.applyStatus] };
|
||||
}
|
||||
return { backgroundColor: data.canApply ? "#07c160" : "#999" };
|
||||
},
|
||||
formatDate(value) {
|
||||
return value ? moment(value).format("YYYY-MM-DD HH:mm") : "";
|
||||
},
|
||||
detailsClick(id) {
|
||||
this.$emit("goDetails", id);
|
||||
},
|
||||
getTagTxt(data) {
|
||||
if (this.isMyApply && data.applyStatus !== undefined) {
|
||||
return this.applyStatusMap[data.applyStatus];
|
||||
}
|
||||
return data.canApply ? "进行中" : "已结束";
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.actItem {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
padding: 15px;
|
||||
background: white;
|
||||
margin-bottom: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.actItem-pic {
|
||||
position: relative;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.actItem-pic img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.actItemStatus {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
padding: 2px 8px;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.actItem-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.actItem-title {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.actItem-tag {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,60 +0,0 @@
|
||||
<template>
|
||||
<ul class="navs">
|
||||
<li>
|
||||
<router-link to="/loveActs">
|
||||
<img src="../../assets/p1.png" />
|
||||
<p>认领活动</p>
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link to="/loveReport">
|
||||
<img src="../../assets/p2.png" />
|
||||
<p>爱心报表</p>
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link to="/UploadService">
|
||||
<img src="../../assets/p3.png" />
|
||||
<p>服务纪实</p>
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link to="/serviceRecords">
|
||||
<img src="../../assets/p5.png" />
|
||||
<p>我的服务</p>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Navs",
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.navs {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding: 20px;
|
||||
background: white;
|
||||
margin: 10px 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.navs li {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.navs img {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.navs a {
|
||||
text-decoration: none;
|
||||
color: #333;
|
||||
}
|
||||
</style>
|
||||
@@ -1,64 +0,0 @@
|
||||
<template>
|
||||
<router-link to="/userCenter" class="header-inner">
|
||||
<div class="title">大学生志愿者活动</div>
|
||||
<div class="user-info-container">
|
||||
<div class="userInfo">
|
||||
<img class="avatar" :src="user.avatar" />
|
||||
<span class="user-name">{{ user.name }}</span>
|
||||
</div>
|
||||
<span class="arrow">></span>
|
||||
</div>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ["user"],
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.header-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 15px;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.user-info-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.userInfo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid white;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
@@ -1,108 +0,0 @@
|
||||
<template>
|
||||
<ul class="overview">
|
||||
<li>
|
||||
<p class="num">
|
||||
<span class="highColor">{{ data.times }}</span>次
|
||||
</p>
|
||||
<span class="label">服务次数</span>
|
||||
</li>
|
||||
<li>
|
||||
<p class="num">
|
||||
<span class="highColor">{{ data.duration }}</span>小时
|
||||
</p>
|
||||
<span class="label">服务时长</span>
|
||||
</li>
|
||||
<li>
|
||||
<p class="num">
|
||||
<span class="highColor">{{ data.score }}</span>分
|
||||
</p>
|
||||
<span class="label">服务积分</span>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
|
||||
export default {
|
||||
props: ["year"],
|
||||
data() {
|
||||
return {
|
||||
data: {},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
fetchYearScore() {
|
||||
const that = this;
|
||||
const year = this.year;
|
||||
axios
|
||||
.get("/api/service/yearScore", {
|
||||
params: { year },
|
||||
})
|
||||
.then(function (response) {
|
||||
const { error, data = {} } = response.data;
|
||||
if (error === 0) {
|
||||
that.data = data || {};
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
year() {
|
||||
this.fetchYearScore();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.overview {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding: 20px 15px;
|
||||
background: white;
|
||||
list-style: none;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.08);
|
||||
margin: 10px 12px 15px;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.overview li {
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.overview li:not(:last-child)::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 1px;
|
||||
height: 40px;
|
||||
background: linear-gradient(to bottom, transparent, #eee, transparent);
|
||||
}
|
||||
|
||||
.num {
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.highColor {
|
||||
color: #ff6b35;
|
||||
font-weight: 700;
|
||||
font-size: 26px;
|
||||
display: block;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
@@ -1,117 +0,0 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="details" @click="detailsClick">
|
||||
<div class="imgBox">
|
||||
<img :src="data.pic" />
|
||||
<p class="status" :style="{ backgroundColor: bgColorMap[data.status] }">
|
||||
{{ statusMap[data.status] }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="main">
|
||||
<p class="content">{{ data.content }}</p>
|
||||
<p class="bottomTxt">
|
||||
<span style="margin-right: 10px">{{ data.publisher }}</span>
|
||||
<span>{{ formatDate(data.time) }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import moment from "moment";
|
||||
|
||||
export default {
|
||||
name: "recordItem",
|
||||
props: ["data"],
|
||||
data() {
|
||||
return {
|
||||
statusMap: ["审核中", "审核通过", "审核驳回"],
|
||||
bgColorMap: ["#1989fa", "#07c160", "#f6352c"],
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
formatDate(value) {
|
||||
return value ? moment(value).format("YYYY-MM-DD HH:mm") : "--";
|
||||
},
|
||||
detailsClick() {
|
||||
this.$emit("goDetails", this.data.id);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.details {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.details:active {
|
||||
transform: scale(0.99);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.imgBox {
|
||||
position: relative;
|
||||
width: 76px;
|
||||
height: 76px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.imgBox img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.status {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 3px 10px;
|
||||
color: white;
|
||||
font-size: 11px;
|
||||
border-radius: 10px 0 8px 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bottomTxt {
|
||||
color: #aaa;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,118 +0,0 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="user">
|
||||
<img class="user-avatar" :src="userData.avatar" />
|
||||
<div>
|
||||
<p class="user-name">{{ userData.name }}</p>
|
||||
<span class="user-score">{{ userData.totalScore }}积分</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<van-button
|
||||
round
|
||||
hairline
|
||||
size="mini"
|
||||
icon="arrow-down"
|
||||
color="#ff5d23"
|
||||
@click="showPicker = true"
|
||||
>
|
||||
{{ date.text }}
|
||||
</van-button>
|
||||
|
||||
<van-popup v-model:show="showPicker" round position="bottom">
|
||||
<van-picker
|
||||
:columns="dateList"
|
||||
@cancel="cancel"
|
||||
@confirm="confirm"
|
||||
/>
|
||||
</van-popup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
|
||||
export default {
|
||||
props: ["dateList", "date"],
|
||||
data() {
|
||||
return {
|
||||
showPicker: false,
|
||||
userData: {},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
fetchUserData() {
|
||||
const that = this;
|
||||
axios.get("/api/service/userScore").then(function (response) {
|
||||
const { error, data = {} } = response.data;
|
||||
if (error === 0) {
|
||||
that.userData = data || {};
|
||||
}
|
||||
});
|
||||
},
|
||||
cancel() {
|
||||
this.showPicker = false;
|
||||
},
|
||||
confirm({ selectedOptions }) {
|
||||
const data = selectedOptions[0];
|
||||
this.showPicker = false;
|
||||
this.$emit("updateYear", data);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fetchUserData();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.user > div,
|
||||
.user > div > p,
|
||||
.user > div > span {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
:deep(.van-button) {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 65px;
|
||||
height: 65px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 6px;
|
||||
color: #fff;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.user-score {
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 13px;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
</style>
|
||||
@@ -1,32 +0,0 @@
|
||||
import { createApp } from "vue";
|
||||
import "./style.css";
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
import {
|
||||
Button,
|
||||
List,
|
||||
Tag,
|
||||
Field,
|
||||
CellGroup,
|
||||
Uploader,
|
||||
Popup,
|
||||
Picker,
|
||||
DatePicker,
|
||||
} from "vant";
|
||||
import "vant/lib/index.css";
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
app
|
||||
.use(Button)
|
||||
.use(List)
|
||||
.use(Tag)
|
||||
.use(Field)
|
||||
.use(CellGroup)
|
||||
.use(Uploader)
|
||||
.use(Popup)
|
||||
.use(Picker)
|
||||
.use(DatePicker)
|
||||
.use(router);
|
||||
|
||||
app.mount("#app");
|
||||
@@ -1,65 +0,0 @@
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
import HomeView from "../views/HomeView.vue";
|
||||
import UserCenter from "../views/UserCenter.vue";
|
||||
import LoveReport from "../views/LoveReport.vue";
|
||||
import LoveActs from "../views/LoveActs.vue";
|
||||
import ActDetails from "../views/ActDetails.vue";
|
||||
import MyApply from "../views/MyApply.vue";
|
||||
import UploadService from "../views/UploadService.vue";
|
||||
import ServiceRecords from "../views/ServiceRecords.vue";
|
||||
import ServiceDetail from "../views/ServiceDetail.vue";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: "/",
|
||||
name: "home",
|
||||
component: HomeView,
|
||||
},
|
||||
{
|
||||
path: "/userCenter",
|
||||
name: "userCenter",
|
||||
component: UserCenter,
|
||||
},
|
||||
{
|
||||
path: "/loveReport",
|
||||
name: "loveReport",
|
||||
component: LoveReport,
|
||||
},
|
||||
{
|
||||
path: "/loveActs",
|
||||
name: "loveActs",
|
||||
component: LoveActs,
|
||||
},
|
||||
{
|
||||
path: "/actDetails",
|
||||
name: "actDetails",
|
||||
component: ActDetails,
|
||||
},
|
||||
{
|
||||
path: "/myApply",
|
||||
name: "myApply",
|
||||
component: MyApply,
|
||||
},
|
||||
{
|
||||
path: "/UploadService",
|
||||
name: "UploadService",
|
||||
component: UploadService,
|
||||
},
|
||||
{
|
||||
path: "/serviceRecords",
|
||||
name: "serviceRecords",
|
||||
component: ServiceRecords,
|
||||
},
|
||||
{
|
||||
path: "/serviceDetail",
|
||||
name: "serviceDetail",
|
||||
component: ServiceDetail,
|
||||
},
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -1,80 +0,0 @@
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
#app {
|
||||
max-width: 414px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
min-height: 100vh;
|
||||
box-shadow: 0 0 15px rgba(0, 0, 0, 0.15);
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="head">
|
||||
<p class="title">{{ data.title }}</p>
|
||||
<div class="status">
|
||||
<van-tag
|
||||
:color="getActStatusColor(data)"
|
||||
round
|
||||
style="margin-right: 10px"
|
||||
>
|
||||
{{ data.canApply ? "进行中" : "已结束" }}
|
||||
</van-tag>
|
||||
<van-tag :color="getActApplyColor(data)" round>
|
||||
{{ applyStatusMap[data.applyStatus] }}
|
||||
</van-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dataItem">
|
||||
<span class="label">活动时间</span>
|
||||
<p>
|
||||
{{ formatDate(data.startTime) }} -
|
||||
{{ formatDate(data.endTime) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="dataItem">
|
||||
<span class="label">活动地点</span>
|
||||
<p>{{ data.place }}</p>
|
||||
</div>
|
||||
|
||||
<div class="dataItem">
|
||||
<span class="label">活动简介</span>
|
||||
<p>{{ data.content }}</p>
|
||||
</div>
|
||||
|
||||
<div class="dataItem">
|
||||
<span class="label">活动来源</span>
|
||||
<p>{{ data.publisher }}</p>
|
||||
</div>
|
||||
|
||||
<div class="dataItem">
|
||||
<span class="label">服务时长</span>
|
||||
<p>{{ data.hour }}小时</p>
|
||||
</div>
|
||||
|
||||
<div class="dataItem">
|
||||
<span class="label">招募人数</span>
|
||||
<p>{{ data.total }}人</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="fBtn"
|
||||
v-if="[2, 3].indexOf(data.applyStatus) === -1"
|
||||
@click="applyClick"
|
||||
>
|
||||
{{ data.applyStatus === 1 ? "撤销申请" : "立即报名" }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import moment from "moment";
|
||||
import { showSuccessToast, showFailToast } from "vant";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
data: {},
|
||||
applyStatusMap: ["", "审核中", "审核通过", "审核拒绝"],
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
formatDate(value) {
|
||||
return value ? moment(value).format("YYYY-MM-DD HH:mm") : "--";
|
||||
},
|
||||
getActStatusColor(data) {
|
||||
return data.canApply ? "#07C160" : "#ff976a";
|
||||
},
|
||||
getActApplyColor(data) {
|
||||
const list = ["", "#1989fa", "#07c160", "#f6352c"];
|
||||
return list[data.applyStatus];
|
||||
},
|
||||
fetchDetails() {
|
||||
const that = this;
|
||||
const payload = { id: this.$route.query.id };
|
||||
axios
|
||||
.get("/api/actDetails/details", {
|
||||
params: payload,
|
||||
})
|
||||
.then(function (response) {
|
||||
const { error, data = {} } = response.data;
|
||||
if (error === 0) {
|
||||
that.data = data.details || {};
|
||||
}
|
||||
});
|
||||
},
|
||||
applyClick() {
|
||||
const that = this;
|
||||
const payload = {
|
||||
isApplay: this.data.applyStatus !== 1,
|
||||
id: this.$route.query.id,
|
||||
};
|
||||
|
||||
axios
|
||||
.post("/api/actDetails/apply", payload)
|
||||
.then(function (response) {
|
||||
const { error, msg } = response.data;
|
||||
if (error === 0) {
|
||||
showSuccessToast("操作成功");
|
||||
if (payload.isApplay === false) {
|
||||
that.data.applyStatus = 0; // Simulate status change to 'not applied'
|
||||
} else {
|
||||
that.data.applyStatus = 1; // Simulate status change to 'under review'
|
||||
}
|
||||
that.fetchDetails();
|
||||
} else {
|
||||
showFailToast(msg || "网络错误,请稍后重试");
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fetchDetails();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.head {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
margin-bottom: 10px;
|
||||
color: #333; /* ✅ 让 head 区域默认文字深色 */
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
color: #333; /* ✅ 标题深色(关键) */
|
||||
}
|
||||
|
||||
.status {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.dataItem {
|
||||
background: white;
|
||||
padding: 15px 20px;
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.dataItem p {
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.fBtn {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
padding: 15px;
|
||||
background: #ff5d23;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
@@ -1,126 +0,0 @@
|
||||
<template>
|
||||
<div class="home">
|
||||
<div class="header">
|
||||
<User :user="userInfo" />
|
||||
</div>
|
||||
<Navs />
|
||||
<div class="list">
|
||||
<div class="list-header">
|
||||
<span>志愿活动</span>
|
||||
<div class="list-more" @click="goMoreActs">
|
||||
查看更多
|
||||
<img src="../assets/rArrow.png" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="actList">
|
||||
<ActItem
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
:data="item"
|
||||
@goDetails="goDetails"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import User from "../components/Home/User.vue";
|
||||
import Navs from "../components/Home/Navs.vue";
|
||||
import ActItem from "../components/Home/ActItem.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
User,
|
||||
Navs,
|
||||
ActItem,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
userInfo: {
|
||||
avatar: "",
|
||||
name: "",
|
||||
},
|
||||
list: [],
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
fetchUserInfo() {
|
||||
const that = this;
|
||||
axios.get("/api/userInfo").then(function (response) {
|
||||
const { error, data = {} } = response.data;
|
||||
if (error === 0) {
|
||||
that.userInfo = {
|
||||
avatar: data.avatar,
|
||||
name: data.name,
|
||||
};
|
||||
}
|
||||
});
|
||||
},
|
||||
fetchActList() {
|
||||
const that = this;
|
||||
axios.get("/api/actList").then(function (response) {
|
||||
const { error, data = {} } = response.data;
|
||||
if (error === 0) {
|
||||
that.list = data.list;
|
||||
}
|
||||
});
|
||||
},
|
||||
goDetails(id) {
|
||||
this.$router.push("/actDetails?id=" + id);
|
||||
},
|
||||
goMoreActs() {
|
||||
this.$router.push("/loveActs");
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fetchUserInfo();
|
||||
this.fetchActList();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.home {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.list-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.list-header span {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.list-more {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.list-more img {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,127 +0,0 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="searchBox">
|
||||
<div class="search">
|
||||
<span class="search-icon"></span>
|
||||
<input
|
||||
placeholder="请输入地点、活动名称等关键字"
|
||||
v-model="keyword"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="body">
|
||||
<van-list
|
||||
:loading="loading"
|
||||
:finished="true"
|
||||
finished-text="没有更多了"
|
||||
>
|
||||
<ActItem
|
||||
v-for="item in filteredActList"
|
||||
:key="item.id"
|
||||
:data="item"
|
||||
@goDetails="goDetails"
|
||||
/>
|
||||
</van-list>
|
||||
</div>
|
||||
|
||||
<button class="fBtn" @click="goMyApply">我的报名</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import ActItem from "../components/Home/ActItem.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ActItem,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
allActList: [], // 用于存储所有活动数据
|
||||
loading: false,
|
||||
keyword: "",
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
// 创建一个计算属性来根据关键字筛选活动列表
|
||||
filteredActList() {
|
||||
if (!this.keyword.trim()) {
|
||||
return this.allActList; // 如果关键字为空,返回所有活动
|
||||
}
|
||||
const searchTerm = this.keyword.trim().toLowerCase();
|
||||
return this.allActList.filter(item => {
|
||||
const title = (item.title || '').toLowerCase();
|
||||
const place = (item.place || '').toLowerCase();
|
||||
return title.includes(searchTerm) || place.includes(searchTerm);
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 获取完整的活动列表
|
||||
fetchActList() {
|
||||
this.loading = true;
|
||||
axios
|
||||
.get("/api/actList")
|
||||
.then(response => {
|
||||
const { error, data = {} } = response.data;
|
||||
if (error === 0) {
|
||||
this.allActList = data.list || [];
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
goMyApply() {
|
||||
this.$router.push("/myApply");
|
||||
},
|
||||
goDetails(id) {
|
||||
this.$router.push("/actDetails?id=" + id);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
// 组件加载时获取所有数据
|
||||
this.fetchActList();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.searchBox {
|
||||
padding: 15px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.search input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
background: transparent;
|
||||
outline: none;
|
||||
margin-left: 10px;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.search input::placeholder {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.body {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.fBtn {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
padding: 12px 24px;
|
||||
background: #ff5d23;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 25px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
@@ -1,224 +0,0 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="tabs">
|
||||
<div
|
||||
:class="{ tabItem: true, active: curTab === 0 }"
|
||||
@click="tabClick(0)"
|
||||
>
|
||||
总积分
|
||||
</div>
|
||||
<div
|
||||
:class="{ tabItem: true, active: curTab === 1 }"
|
||||
@click="tabClick(1)"
|
||||
>
|
||||
年度积分
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dataList">
|
||||
<div class="dataItem">
|
||||
<p class="dataItem-score">{{ provinceRank }}</p>
|
||||
<p class="dataItem-label">全省的排名</p>
|
||||
</div>
|
||||
<div class="dataItem">
|
||||
<p class="dataItem-score">{{ totalScore }}</p>
|
||||
<p class="dataItem-label">{{ scoreLabel }}</p>
|
||||
</div>
|
||||
<div class="dataItem">
|
||||
<p class="dataItem-score">{{ grandeRank }}</p>
|
||||
<p class="dataItem-label">本校本年级排名</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rank">
|
||||
<div class="rank-hd">
|
||||
<p class="rank-title">本校本年级服务标兵</p>
|
||||
<p class="rank-range">1200人参与排名</p>
|
||||
</div>
|
||||
<ul class="rank-list">
|
||||
<li v-for="(item, idx) in dataList" :key="item.id">
|
||||
<div class="rank-student">
|
||||
<span class="rank-idx">{{ idx + 1 }}</span>
|
||||
<span class="rank-avatar">
|
||||
<img :src="item.avatar" />
|
||||
</span>
|
||||
<span class="rank-name">{{ item.name }}</span>
|
||||
</div>
|
||||
<div class="rank-score">{{ item.score }}分</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
curTab: 0,
|
||||
dataList: [],
|
||||
provinceRank: "",
|
||||
totalScore: "",
|
||||
grandeRank: "",
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
scoreLabel() {
|
||||
return this.curTab === 0 ? '服务总积分' : '服务年度积分';
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
tabClick(value) {
|
||||
this.curTab = Number(value);
|
||||
this.fetchOverview(this.curTab);
|
||||
this.fetchRankList(this.curTab);
|
||||
},
|
||||
fetchOverview(type) {
|
||||
const that = this;
|
||||
axios
|
||||
.get("/api/report/myOverview", {
|
||||
params: { type },
|
||||
})
|
||||
.then(function (response) {
|
||||
const { error, data = {} } = response.data;
|
||||
if (error === 0) {
|
||||
that.provinceRank = data.provinceRank;
|
||||
that.totalScore = data.totalScore;
|
||||
that.grandeRank = data.grandeRank;
|
||||
}
|
||||
});
|
||||
},
|
||||
fetchRankList(type) {
|
||||
const that = this;
|
||||
const payload = {
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
type,
|
||||
};
|
||||
axios
|
||||
.get("/api/report/rankList", { params: payload })
|
||||
.then(function (response) {
|
||||
const { error, data = {} } = response.data;
|
||||
if (error === 0) {
|
||||
that.dataList = data.list || [];
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fetchOverview(this.curTab);
|
||||
this.fetchRankList(this.curTab);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
background-color: #f7f8fa;
|
||||
padding: 10px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.tabs,
|
||||
.dataList,
|
||||
.rank {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 10px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.tabItem {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.tabItem.active {
|
||||
color: #ff5d23;
|
||||
border-bottom: 2px solid #ff5d23;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.dataList {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.dataItem {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dataItem-score {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #ff5d23;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.dataItem-label {
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.rank {
|
||||
padding: 15px;
|
||||
color: #333; /* ⭐ 内容区统一深色 */
|
||||
}
|
||||
|
||||
.rank-hd {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.rank-title {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.rank-range {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.rank-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0 auto; /* Centering the list */
|
||||
width: 100%;
|
||||
max-width: 300px; /* Constrain width */
|
||||
}
|
||||
|
||||
.rank-list li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.rank-idx {
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.rank-name {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.rank-score {
|
||||
color: #ff5d23;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
@@ -1,116 +0,0 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<ul class="tabList">
|
||||
<li @click="tabClick(0)">
|
||||
<span :class="curTab === 0 ? 'active' : ''">全部</span>
|
||||
</li>
|
||||
<li @click="tabClick(1)">
|
||||
<span :class="curTab === 1 ? 'active' : ''">审核中</span>
|
||||
</li>
|
||||
<li @click="tabClick(2)">
|
||||
<span :class="curTab === 2 ? 'active' : ''">审核通过</span>
|
||||
</li>
|
||||
<li @click="tabClick(3)">
|
||||
<span :class="curTab === 3 ? 'active' : ''">审核拒绝</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="list">
|
||||
<van-list
|
||||
v-model:loading="loading"
|
||||
:finished="true"
|
||||
finished-text="没有更多了"
|
||||
>
|
||||
<ActItem
|
||||
v-for="item in actList"
|
||||
:key="item.id"
|
||||
:data="item"
|
||||
:isMyApply="true"
|
||||
@goDetails="goDetails"
|
||||
/>
|
||||
</van-list>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import ActItem from "../components/Home/ActItem.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ActItem,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
curTab: 0,
|
||||
allActList: [],
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
actList() {
|
||||
if (this.curTab === 0) {
|
||||
return this.allActList;
|
||||
}
|
||||
return this.allActList.filter(item => item.applyStatus === this.curTab);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
tabClick(value) {
|
||||
this.curTab = value;
|
||||
},
|
||||
fetchApplyList() {
|
||||
this.loading = true;
|
||||
axios
|
||||
.get("/api/myApplyList")
|
||||
.then((response) => {
|
||||
const { error, data = {} } = response.data;
|
||||
if (error === 0) {
|
||||
this.allActList = data.list || [];
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
goDetails(id) {
|
||||
this.$router.push("/actDetails?id=" + id);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fetchApplyList();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tabList {
|
||||
display: flex;
|
||||
background: white;
|
||||
list-style: none;
|
||||
border-bottom: 1px solid #eee;
|
||||
color: #333; /* ✅ tab 区域默认文字深色 */
|
||||
}
|
||||
|
||||
.tabList li {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 15px 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tabList span {
|
||||
padding: 5px 10px;
|
||||
color: #333; /* ✅ 兜底:span 明确深色 */
|
||||
}
|
||||
|
||||
.tabList span.active {
|
||||
color: #ff5d23;
|
||||
border-bottom: 2px solid #ff5d23;
|
||||
}
|
||||
|
||||
.list {
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,153 +0,0 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="dataItem">
|
||||
<span class="label">服务时间</span>
|
||||
<span>{{ formatDay(data.actTime) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="dataItem">
|
||||
<span class="label">服务地点</span>
|
||||
<span>{{ data.place }}</span>
|
||||
</div>
|
||||
|
||||
<div class="dataItem">
|
||||
<span class="label">服务内容</span>
|
||||
<span>{{ data.content }}</span>
|
||||
</div>
|
||||
|
||||
<div class="dataItem">
|
||||
<span class="label">服务照片</span>
|
||||
<div>
|
||||
<img
|
||||
class="serviceImg"
|
||||
v-for="url in data.imgList"
|
||||
:src="url"
|
||||
:key="url"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dataItem">
|
||||
<span class="label">服务时长</span>
|
||||
<span>{{ data.hour }}小时</span>
|
||||
</div>
|
||||
|
||||
<div class="dataItem">
|
||||
<span class="label">纪实时间</span>
|
||||
<span>{{ formatDate(data.uploadTime) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="dataItem">
|
||||
<span class="label">审核状态</span>
|
||||
<span>
|
||||
<span
|
||||
class="status"
|
||||
:style="{ backgroundColor: bgColorMap[data.status] }"
|
||||
>
|
||||
{{ statusMap[data.status] }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p class="title">{{ titleMap[data.status] }}</p>
|
||||
<p class="score" v-if="data.status === 1">4分</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import moment from "moment";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
data: {},
|
||||
statusMap: ["审核中", "审核通过", "审核驳回"],
|
||||
bgColorMap: ["#1989fa", "#07c160", "#f6352c"],
|
||||
titleMap: [
|
||||
"正在审核中,请耐心等待",
|
||||
"感谢您的服务,请收下服务积分",
|
||||
"审核被驳回了哟",
|
||||
],
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
formatDate(value) {
|
||||
return value ? moment(value).format("YYYY-MM-DD HH:mm") : "--";
|
||||
},
|
||||
formatDay(value) {
|
||||
return value ? moment(value).format("YYYY-MM-DD") : "--";
|
||||
},
|
||||
fetchDetails() {
|
||||
const that = this;
|
||||
const payload = { id: this.$route.query.id };
|
||||
axios
|
||||
.get("/api/service/details", {
|
||||
params: payload,
|
||||
})
|
||||
.then(function (response) {
|
||||
const { error, data = {} } = response.data;
|
||||
if (error === 0) {
|
||||
that.data = data.details || {};
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fetchDetails();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dataItem {
|
||||
background: white;
|
||||
padding: 15px 20px;
|
||||
margin-bottom: 1px;
|
||||
color: #333; /* ✅ 关键:让值的文字变深色 */
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.serviceImg {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
margin-right: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: 4px 12px;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.score {
|
||||
font-size: 32px;
|
||||
color: #ff5d23;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
@@ -1,133 +0,0 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<User @updateYear="updateYear" :date="year" :dateList="dateList" />
|
||||
</div>
|
||||
|
||||
<Overview :year="year.value" />
|
||||
|
||||
<van-list
|
||||
v-model:loading="loading"
|
||||
:finished="finished"
|
||||
finished-text="没有更多了"
|
||||
@load="onLoad"
|
||||
>
|
||||
<RecordItem
|
||||
v-for="item in dataList"
|
||||
:key="item.id"
|
||||
:data="item"
|
||||
@goDetails="goDetails"
|
||||
/>
|
||||
</van-list>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import User from "../components/ServiceRecords/User.vue";
|
||||
import Overview from "../components/ServiceRecords/Overview.vue";
|
||||
import RecordItem from "../components/ServiceRecords/RecordItem.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
User,
|
||||
Overview,
|
||||
RecordItem,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dataList: [],
|
||||
loading: false,
|
||||
finished: false,
|
||||
currentPage: 1,
|
||||
year: {},
|
||||
dateList: [],
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
fetchYearList() {
|
||||
const that = this;
|
||||
axios.get("/api/service/yearList").then(function (response) {
|
||||
const { error, data = {} } = response.data;
|
||||
if (error === 0) {
|
||||
const list = data.list || [];
|
||||
that.dateList = [{ value: 'all', text: '全部' }, ...list];
|
||||
that.year = that.dateList[0];
|
||||
}
|
||||
});
|
||||
},
|
||||
fetchServiceRecords(currentPage = 1) {
|
||||
const that = this;
|
||||
const payload = {
|
||||
currentPage,
|
||||
pageSize: 10,
|
||||
};
|
||||
if (this.year.value !== 'all') {
|
||||
payload.year = this.year.value;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
axios
|
||||
.get("/api/service/list", { params: payload })
|
||||
.then(function (response) {
|
||||
const { error, data = {} } = response.data;
|
||||
if (error === 0) {
|
||||
const currentPage = data.current;
|
||||
const list = data.list;
|
||||
|
||||
if (currentPage === 1) {
|
||||
that.dataList = list;
|
||||
} else {
|
||||
that.dataList.push(...list);
|
||||
}
|
||||
|
||||
that.currentPage = currentPage;
|
||||
that.finished = data.pageCount === currentPage;
|
||||
}
|
||||
})
|
||||
.finally(function () {
|
||||
that.loading = false;
|
||||
});
|
||||
},
|
||||
onLoad() {
|
||||
this.fetchServiceRecords(this.currentPage + 1);
|
||||
},
|
||||
goDetails(id) {
|
||||
this.$router.push("/serviceDetail?id=" + id);
|
||||
},
|
||||
updateYear(yearValue) {
|
||||
this.year = yearValue;
|
||||
this.currentPage = 1;
|
||||
this.fetchServiceRecords(1);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fetchYearList();
|
||||
},
|
||||
watch: {
|
||||
"year.value"() {
|
||||
if (this.year.value) {
|
||||
this.currentPage = 1;
|
||||
this.fetchServiceRecords(1);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
background-color: #f5f6f8;
|
||||
min-height: auto;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #ff6b35 0%, #f7931e 50%, #ffb347 100%);
|
||||
color: white;
|
||||
padding: 20px 12px 25px;
|
||||
border-radius: 0 0 30px 30px;
|
||||
box-shadow: 0 4px 20px rgba(255, 107, 53, 0.3);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,240 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="frm">
|
||||
<van-field
|
||||
v-model="publisher.text"
|
||||
is-link
|
||||
readonly
|
||||
label="活动来源"
|
||||
placeholder="请选择活动来源"
|
||||
@click="fieldCLick(0)"
|
||||
/>
|
||||
|
||||
<van-field
|
||||
v-model="date"
|
||||
is-link
|
||||
readonly
|
||||
label="服务时间"
|
||||
placeholder="请选择日期"
|
||||
@click="fieldCLick(1)"
|
||||
/>
|
||||
|
||||
<van-field
|
||||
v-model="duration.text"
|
||||
is-link
|
||||
readonly
|
||||
label="服务时长"
|
||||
placeholder="请选择时长"
|
||||
@click="fieldCLick(2)"
|
||||
/>
|
||||
|
||||
<van-field
|
||||
v-model="content"
|
||||
label="服务内容"
|
||||
placeholder="请对服务内容进行简要描述"
|
||||
label-align="top"
|
||||
type="textarea"
|
||||
maxlength="100"
|
||||
show-word-limit
|
||||
/>
|
||||
|
||||
<van-uploader
|
||||
v-model="fileList"
|
||||
multiple
|
||||
max-count="2"
|
||||
:upload-text="uploadTxt"
|
||||
image-fit="cover"
|
||||
style="padding: 10px"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<van-button
|
||||
type="primary"
|
||||
color="#ff5d23"
|
||||
block
|
||||
@click="submit"
|
||||
>
|
||||
提交
|
||||
</van-button>
|
||||
</div>
|
||||
|
||||
<van-popup v-model:show="showPicker" round position="bottom">
|
||||
<van-picker
|
||||
:columns="columns"
|
||||
@cancel="pickerCancel"
|
||||
@confirm="pickerConfirm"
|
||||
/>
|
||||
</van-popup>
|
||||
|
||||
<van-popup v-model:show="showDatePicker" round position="bottom">
|
||||
<van-date-picker
|
||||
v-model="currentDate"
|
||||
title="选择日期"
|
||||
:min-date="minDate"
|
||||
:max-date="maxDate"
|
||||
@cancel="pickerCancel"
|
||||
@confirm="pickerConfirm"
|
||||
/>
|
||||
</van-popup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { showToast, showSuccessToast, showFailToast } from "vant";
|
||||
import axios from "axios";
|
||||
|
||||
function dateRange() {
|
||||
const curTimeStamp = new Date();
|
||||
const tYear = curTimeStamp.getFullYear();
|
||||
const tMonth = curTimeStamp.getMonth();
|
||||
const date = curTimeStamp.getDate();
|
||||
return {
|
||||
currentDate: [tYear, tMonth, date],
|
||||
minDate: new Date(tYear - 2, tMonth, date),
|
||||
maxDate: new Date(tYear, tMonth, date),
|
||||
};
|
||||
}
|
||||
|
||||
let publisherList = [];
|
||||
let durationList = [];
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
columns: [],
|
||||
pickerId: null,
|
||||
publisher: { value: null, text: "" },
|
||||
duration: { value: null, text: "" },
|
||||
date: null,
|
||||
content: "",
|
||||
fileList: [],
|
||||
currentDate: null,
|
||||
minDate: null,
|
||||
maxDate: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
showPicker() {
|
||||
return this.pickerId === 0 || this.pickerId === 2;
|
||||
},
|
||||
showDatePicker() {
|
||||
return this.pickerId === 1;
|
||||
},
|
||||
uploadTxt() {
|
||||
const length = this.fileList.length;
|
||||
return "添加照片" + length + "/2";
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
fieldCLick(value) {
|
||||
this.pickerId = value;
|
||||
if (value === 0) {
|
||||
this.columns = publisherList;
|
||||
} else if (value === 2) {
|
||||
this.columns = durationList;
|
||||
}
|
||||
},
|
||||
pickerCancel() {
|
||||
this.pickerId = null;
|
||||
},
|
||||
pickerConfirm({ selectedValues, selectedOptions }) {
|
||||
const curPickerId = this.pickerId;
|
||||
this.pickerId = null;
|
||||
|
||||
if (curPickerId === 1) {
|
||||
const data = selectedValues;
|
||||
const [year, month, day] = data;
|
||||
this.date = `${year}-${month}-${day}`;
|
||||
return;
|
||||
}
|
||||
|
||||
const data = selectedOptions[0];
|
||||
const payload = {
|
||||
value: data.value,
|
||||
text: data.text,
|
||||
};
|
||||
|
||||
if (curPickerId === 0) {
|
||||
this.publisher = payload;
|
||||
return;
|
||||
}
|
||||
|
||||
this.duration = payload;
|
||||
},
|
||||
fetchPublisherlist() {
|
||||
axios.get("/api/act/publisherList").then(function (response) {
|
||||
const { error, data = {} } = response.data;
|
||||
if (error === 0) {
|
||||
publisherList = data.list || [];
|
||||
}
|
||||
});
|
||||
},
|
||||
fetchDurations() {
|
||||
axios.get("/api/act/durationsList").then(function (response) {
|
||||
const { error, data = {} } = response.data;
|
||||
if (error === 0) {
|
||||
durationList = data.list || [];
|
||||
}
|
||||
});
|
||||
},
|
||||
submit() {
|
||||
if (!this.publisher.text) {
|
||||
showToast("未选择活动来源");
|
||||
return;
|
||||
}
|
||||
if (!this.date) {
|
||||
showToast("未选择服务时间");
|
||||
return;
|
||||
}
|
||||
if (!this.duration.text) {
|
||||
showToast("未选择服务时长");
|
||||
return;
|
||||
}
|
||||
if (!this.content) {
|
||||
showToast("未填写服务内容");
|
||||
return;
|
||||
}
|
||||
if (!this.fileList.length) {
|
||||
showToast("未上传服务照片");
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
publisher: this.publisher.value,
|
||||
date: +new Date(this.date),
|
||||
duration: this.duration.value,
|
||||
content: this.content,
|
||||
fileList: this.fileList,
|
||||
};
|
||||
|
||||
axios.post("/api/act/uploadService", payload).then(function (response) {
|
||||
const { error, msg } = response.data;
|
||||
if (error === 0) {
|
||||
showSuccessToast("操作成功");
|
||||
} else {
|
||||
showFailToast(msg || "网络错误,请稍后重试");
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
created() {
|
||||
const { currentDate, minDate, maxDate } = dateRange();
|
||||
this.currentDate = currentDate;
|
||||
this.minDate = minDate;
|
||||
this.maxDate = maxDate;
|
||||
this.fetchDurations();
|
||||
this.fetchPublisherlist();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.frm {
|
||||
background: white;
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,311 +0,0 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="inner">
|
||||
<h1>个人信息</h1>
|
||||
<ul>
|
||||
<li>
|
||||
<p class="label"><span>*</span>姓名</p>
|
||||
<input placeholder="请输入姓名" v-model.trim="name" />
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<p class="label"><span>*</span>编号</p>
|
||||
<input placeholder="请输入编号" v-model.trim="codeStr" />
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<p class="label"><span>*</span>性别</p>
|
||||
<input type="radio" value="0" v-model="gender" />男
|
||||
<input type="radio" value="1" v-model="gender" />女
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<p class="label"><span>*</span>手机号</p>
|
||||
<input placeholder="请输入手机号" v-model.trim="phoneNum" />
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<p class="label"><span>*</span>学校</p>
|
||||
<input placeholder="请输入学校名称" v-model.trim="school" />
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<p class="label"><span>*</span>专业</p>
|
||||
<input placeholder="请输入专业名称" v-model.trim="profession" />
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<p class="label"><span>*</span>人员属性</p>
|
||||
<div class="userTypeList">
|
||||
<span :class="{ active: userType === 0 }" @click="changeUserType(0)">群众</span>
|
||||
<span :class="{ active: userType === 1 }" @click="changeUserType(1)">团员</span>
|
||||
<span :class="{ active: userType === 2 }" @click="changeUserType(2)">党员</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<button class="subBtn" :disabled="saving" @click="submitClick">
|
||||
{{ saving ? "保存中..." : "保存" }}
|
||||
</button>
|
||||
|
||||
<button class="subBtn subBtn-ghost" :disabled="saving" @click="resetClick">
|
||||
取消修改
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import { showToast, showSuccessToast, showFailToast } from "vant";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
name: "",
|
||||
code: null,
|
||||
codeStr: "",
|
||||
|
||||
gender: "0",
|
||||
phoneNum: "",
|
||||
school: "",
|
||||
profession: "",
|
||||
userType: 0,
|
||||
|
||||
saving: false,
|
||||
original: null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
changeUserType(value) {
|
||||
this.userType = value;
|
||||
},
|
||||
|
||||
snapshot() {
|
||||
return {
|
||||
name: this.name,
|
||||
code: this.code,
|
||||
codeStr: this.codeStr,
|
||||
gender: this.gender,
|
||||
phoneNum: this.phoneNum,
|
||||
school: this.school,
|
||||
profession: this.profession,
|
||||
userType: this.userType,
|
||||
};
|
||||
},
|
||||
|
||||
applyData(data = {}) {
|
||||
this.name = data.name ?? "";
|
||||
this.code = data.code ?? null;
|
||||
this.codeStr = data.code != null ? String(data.code) : "";
|
||||
|
||||
this.gender = data.gender != null ? String(data.gender) : "0";
|
||||
|
||||
this.phoneNum = data.phoneNum ?? "";
|
||||
this.school = data.school ?? "";
|
||||
this.profession = data.profession ?? "";
|
||||
this.userType = data.userType ?? 0;
|
||||
},
|
||||
|
||||
fetchUsrInfo() {
|
||||
axios
|
||||
.get("/api/userInfo")
|
||||
.then((response) => {
|
||||
const { error, data = {}, msg } = response.data || {};
|
||||
if (error === 0) {
|
||||
this.applyData(data);
|
||||
this.original = this.snapshot();
|
||||
} else {
|
||||
showFailToast(msg || "获取用户信息失败");
|
||||
}
|
||||
})
|
||||
.catch(() => showFailToast("网络错误,请稍后重试"));
|
||||
},
|
||||
|
||||
check() {
|
||||
const name = this.name.trim();
|
||||
const codeStr = this.codeStr.trim();
|
||||
const phoneNum = this.phoneNum.trim();
|
||||
const school = this.school.trim();
|
||||
const profession = this.profession.trim();
|
||||
|
||||
if (!name) {
|
||||
showToast("姓名不可为空");
|
||||
return;
|
||||
}
|
||||
if (!codeStr) {
|
||||
showToast("编号不可为空");
|
||||
return;
|
||||
}
|
||||
if (!phoneNum) {
|
||||
showToast("手机号不可为空");
|
||||
return;
|
||||
}
|
||||
if (!school) {
|
||||
showToast("学校不可为空");
|
||||
return;
|
||||
}
|
||||
if (!profession) {
|
||||
showToast("专业不可为空");
|
||||
return;
|
||||
}
|
||||
|
||||
const codeNum = Number(codeStr);
|
||||
const code = Number.isFinite(codeNum) ? codeNum : codeStr;
|
||||
|
||||
const payload = {
|
||||
name,
|
||||
code,
|
||||
gender: Number(this.gender),
|
||||
phoneNum,
|
||||
school,
|
||||
profession,
|
||||
userType: this.userType,
|
||||
};
|
||||
return payload;
|
||||
},
|
||||
|
||||
submitClick() {
|
||||
const payload = this.check();
|
||||
if (!payload) return;
|
||||
if (this.saving) return;
|
||||
|
||||
this.saving = true;
|
||||
axios
|
||||
.post("/api/userInfo/submit", payload)
|
||||
.then((response) => {
|
||||
const { error, msg } = response.data || {};
|
||||
if (error === 0) {
|
||||
showSuccessToast("保存成功");
|
||||
this.original = this.snapshot();
|
||||
} else {
|
||||
showFailToast(msg || "网络错误,请稍后重试");
|
||||
}
|
||||
})
|
||||
.catch(() => showFailToast("网络错误,请稍后重试"))
|
||||
.finally(() => {
|
||||
this.saving = false;
|
||||
});
|
||||
},
|
||||
|
||||
resetClick() {
|
||||
if (!this.original) return;
|
||||
this.name = this.original.name;
|
||||
this.code = this.original.code;
|
||||
this.codeStr = this.original.codeStr;
|
||||
this.gender = this.original.gender;
|
||||
this.phoneNum = this.original.phoneNum;
|
||||
this.school = this.original.school;
|
||||
this.profession = this.original.profession;
|
||||
this.userType = this.original.userType;
|
||||
showToast("已还原");
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fetchUsrInfo();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.inner {
|
||||
padding: 20px;
|
||||
background: white;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
padding: 15px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.label span {
|
||||
color: red;
|
||||
}
|
||||
|
||||
li input:not([type="radio"]) {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
margin-top: 5px;
|
||||
box-sizing: border-box;
|
||||
|
||||
color: #333;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
li input:not([type="radio"])::placeholder {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
input[type="radio"] {
|
||||
margin-right: 6px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
input[type="radio"]:first-of-type {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.userTypeList {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.userTypeList span {
|
||||
padding: 8px 20px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.userTypeList span.active {
|
||||
background: #ff5d23;
|
||||
color: white;
|
||||
border-color: #ff5d23;
|
||||
}
|
||||
|
||||
.subBtn {
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
background: #ff5d23;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
margin-top: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.subBtn:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.subBtn-ghost {
|
||||
background: transparent;
|
||||
color: #333;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
</style>
|
||||
@@ -1,428 +0,0 @@
|
||||
import { defineConfig } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import VueDevTools from "vite-plugin-vue-devtools";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
function mockPlugin() {
|
||||
const mockDir = path.resolve(process.cwd(), "mock");
|
||||
|
||||
const filePath = (file) => path.resolve(mockDir, file);
|
||||
|
||||
const send = (res, obj, code = 200) => {
|
||||
res.statusCode = code;
|
||||
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
||||
res.end(JSON.stringify(obj));
|
||||
};
|
||||
|
||||
const readJson = (file, fallback) => {
|
||||
const p = filePath(file);
|
||||
if (!fs.existsSync(p)) return fallback;
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(p, "utf-8") || "null") ?? fallback;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
};
|
||||
|
||||
const writeJson = (file, data) => {
|
||||
const p = filePath(file);
|
||||
fs.writeFileSync(p, JSON.stringify(data, null, 2), "utf-8");
|
||||
};
|
||||
|
||||
const readBodyJson = (req) =>
|
||||
new Promise((resolve, reject) => {
|
||||
let body = "";
|
||||
req.on("data", (c) => (body += c));
|
||||
req.on("end", () => {
|
||||
try {
|
||||
resolve(JSON.parse(body || "{}"));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const getPathname = (req) => {
|
||||
try {
|
||||
return new URL(req.url, "http://localhost").pathname;
|
||||
} catch {
|
||||
return req.url;
|
||||
}
|
||||
};
|
||||
|
||||
// 统一 success
|
||||
const ok = (res) => send(res, { error: 0, msg: "success" });
|
||||
|
||||
// ===== 业务联动:积分更新规则 =====
|
||||
// yearScore: score=duration*2,看起来是 1小时=2分
|
||||
const POINTS_PER_HOUR = 2;
|
||||
|
||||
const addScore = (deltaHour) => {
|
||||
const deltaScore = (Number(deltaHour) || 0) * POINTS_PER_HOUR;
|
||||
|
||||
// userInfo.json
|
||||
const ui = readJson("userInfo.json", { error: 0, data: {} });
|
||||
ui.data = ui.data || {};
|
||||
ui.data.totalScore = (Number(ui.data.totalScore) || 0) + deltaScore;
|
||||
writeJson("userInfo.json", ui);
|
||||
|
||||
// userScore.json
|
||||
const us = readJson("userScore.json", { error: 0, data: {} });
|
||||
us.data = us.data || {};
|
||||
us.data.totalScore = (Number(us.data.totalScore) || 0) + deltaScore;
|
||||
writeJson("userScore.json", us);
|
||||
|
||||
// scoreOverview.json
|
||||
const ov = readJson("scoreOverview.json", { error: 0, data: {} });
|
||||
ov.data = ov.data || {};
|
||||
ov.data.totalScore = (Number(ov.data.totalScore) || 0) + deltaScore;
|
||||
writeJson("scoreOverview.json", ov);
|
||||
|
||||
// yearScore.json
|
||||
const ys = readJson("yearScore.json", { error: 0, data: {} });
|
||||
ys.data = ys.data || {};
|
||||
ys.data.times = (Number(ys.data.times) || 0) + 1;
|
||||
ys.data.duration =
|
||||
(Number(ys.data.duration) || 0) + (Number(deltaHour) || 0);
|
||||
ys.data.score = (Number(ys.data.score) || 0) + deltaScore;
|
||||
writeJson("yearScore.json", ys);
|
||||
};
|
||||
|
||||
return {
|
||||
name: "simple-mock-plugin",
|
||||
configureServer(server) {
|
||||
server.middlewares.use(async (req, res, next) => {
|
||||
const pathname = getPathname(req);
|
||||
|
||||
// ========== GET:仍然走你原来的静态文件 ==========
|
||||
const sendJSON = (file) => {
|
||||
const p = filePath(file);
|
||||
if (!fs.existsSync(p)) {
|
||||
return send(res, { error: `Mock file not found: ${file}` }, 404);
|
||||
}
|
||||
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
||||
res.end(fs.readFileSync(p, "utf-8"));
|
||||
};
|
||||
|
||||
if (pathname === "/api/userInfo" && req.method === "GET")
|
||||
return sendJSON("userInfo.json");
|
||||
if (pathname.startsWith("/api/actList") && req.method === "GET")
|
||||
return sendJSON("actList.json");
|
||||
if (
|
||||
pathname.startsWith("/api/report/myOverview") &&
|
||||
req.method === "GET"
|
||||
)
|
||||
return sendJSON("scoreOverview.json");
|
||||
if (pathname.startsWith("/api/report/rankList") && req.method === "GET")
|
||||
return sendJSON("rank.json");
|
||||
if (
|
||||
pathname.startsWith("/api/actDetails/details") &&
|
||||
req.method === "GET"
|
||||
) {
|
||||
const url = new URL(req.url, `http://${req.headers.host}`);
|
||||
const id = Number(url.searchParams.get("id"));
|
||||
|
||||
if (!id) {
|
||||
return send(res, { error: 1, msg: "missing id parameter" }, 400);
|
||||
}
|
||||
|
||||
const actList = readJson("actList.json", {
|
||||
error: 0,
|
||||
data: { list: [] },
|
||||
});
|
||||
const act = (actList.data?.list || []).find((x) => Number(x.id) === id);
|
||||
|
||||
if (!act) {
|
||||
return send(res, { error: 1, msg: "activity not found" }, 404);
|
||||
}
|
||||
|
||||
const actDetails = readJson("actDetails.json", {
|
||||
error: 0,
|
||||
data: { details: {} },
|
||||
});
|
||||
const detailsTemplate = actDetails.data?.details || {};
|
||||
|
||||
const result = {
|
||||
error: 0,
|
||||
data: {
|
||||
details: {
|
||||
id: act.id,
|
||||
title: act.title,
|
||||
startTime: act.startTime,
|
||||
endTime: act.endTime,
|
||||
place: act.place,
|
||||
content: detailsTemplate.content || "活动详情待补充",
|
||||
publisher: detailsTemplate.publisher || "福州大学至诚学院团委",
|
||||
hour: detailsTemplate.hour || 4,
|
||||
total: detailsTemplate.total || 50,
|
||||
canApply: act.canApply,
|
||||
applyStatus: act.applyStatus || 0,
|
||||
actPic: act.actPic
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return send(res, result);
|
||||
}
|
||||
if (pathname.startsWith("/api/myApplyList") && req.method === "GET")
|
||||
return sendJSON("myApplyList.json");
|
||||
if (
|
||||
pathname.startsWith("/api/act/publisherList") &&
|
||||
req.method === "GET"
|
||||
)
|
||||
return sendJSON("publishers.json");
|
||||
if (
|
||||
pathname.startsWith("/api/act/durationsList") &&
|
||||
req.method === "GET"
|
||||
)
|
||||
return sendJSON("durationList.json");
|
||||
if (
|
||||
pathname.startsWith("/api/service/userScore") &&
|
||||
req.method === "GET"
|
||||
)
|
||||
return sendJSON("userScore.json");
|
||||
if (
|
||||
pathname.startsWith("/api/service/yearList") &&
|
||||
req.method === "GET"
|
||||
)
|
||||
return sendJSON("yearList.json");
|
||||
if (
|
||||
pathname.startsWith("/api/service/yearScore") &&
|
||||
req.method === "GET"
|
||||
)
|
||||
return sendJSON("yearScore.json");
|
||||
if (pathname.startsWith("/api/service/list") && req.method === "GET") {
|
||||
const sl = readJson("serviceList.json", {
|
||||
error: 0,
|
||||
data: { current: 1, pageSize: 10, pageCount: 1, list: [] },
|
||||
});
|
||||
const url = new URL(req.url, `http://${req.headers.host}`);
|
||||
const year = url.searchParams.get("year");
|
||||
let list = sl.data?.list || [];
|
||||
if (year && year !== "all") {
|
||||
const yearNum = Number(year);
|
||||
list = list.filter((item) => {
|
||||
const date = new Date(item.time);
|
||||
return date.getFullYear() === yearNum;
|
||||
});
|
||||
}
|
||||
const result = {
|
||||
error: 0,
|
||||
data: {
|
||||
...sl.data,
|
||||
list,
|
||||
current: 1,
|
||||
pageCount: 1,
|
||||
},
|
||||
};
|
||||
return send(res, result);
|
||||
}
|
||||
if (pathname.startsWith("/api/service/details") && req.method === "GET") {
|
||||
const url = new URL(req.url, `http://${req.headers.host}`);
|
||||
const id = Number(url.searchParams.get("id"));
|
||||
|
||||
if (!id) {
|
||||
return send(res, { error: 1, msg: "missing id parameter" }, 400);
|
||||
}
|
||||
|
||||
const sl = readJson("serviceList.json", {
|
||||
error: 0,
|
||||
data: { current: 1, pageSize: 10, pageCount: 1, list: [] },
|
||||
});
|
||||
const record = (sl.data?.list || []).find((x) => Number(x.id) === id);
|
||||
|
||||
if (!record) {
|
||||
return send(res, { error: 1, msg: "record not found" }, 404);
|
||||
}
|
||||
|
||||
let placeText = record.publisher || "未知来源";
|
||||
if (typeof placeText === "number") {
|
||||
const publishers = readJson("publishers.json", {
|
||||
error: 0,
|
||||
data: { list: [] },
|
||||
});
|
||||
const publisherItem = (publishers.data?.list || []).find(
|
||||
(x) => x.value === placeText
|
||||
);
|
||||
placeText = publisherItem?.text || "未知来源";
|
||||
}
|
||||
|
||||
const result = {
|
||||
error: 0,
|
||||
data: {
|
||||
details: {
|
||||
actTime: record.time,
|
||||
place: placeText,
|
||||
content: record.content || "",
|
||||
imgList: record.imgList || [record.pic || "./imgs/actImg.jpeg"],
|
||||
hour: record.hour || 0,
|
||||
uploadTime: record.uploadTime || record.time || Date.now(),
|
||||
status: record.status || 0
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return send(res, result);
|
||||
}
|
||||
|
||||
// ========== POST:全部改成“落盘” ==========
|
||||
// 1) 用户信息保存
|
||||
if (pathname === "/api/userInfo/submit" && req.method === "POST") {
|
||||
try {
|
||||
const incoming = await readBodyJson(req); // {name,code,gender,phoneNum,school,profession,userType...}
|
||||
const old = readJson("userInfo.json", { error: 0, data: {} });
|
||||
const next = {
|
||||
error: 0,
|
||||
data: { ...(old.data || {}), ...incoming },
|
||||
};
|
||||
writeJson("userInfo.json", next);
|
||||
return ok(res);
|
||||
} catch {
|
||||
return send(res, { error: 1, msg: "Invalid JSON body" }, 400);
|
||||
}
|
||||
}
|
||||
|
||||
// 2) 申请/撤销活动:更新 myApplyList + actDetails(可选 actList)
|
||||
if (
|
||||
pathname.startsWith("/api/actDetails/apply") &&
|
||||
req.method === "POST"
|
||||
) {
|
||||
try {
|
||||
const incoming = await readBodyJson(req);
|
||||
// 约定:至少要有 id;可选传 action: "apply" | "cancel"
|
||||
const id = Number(incoming.id ?? incoming.actId);
|
||||
const action = incoming.action || "apply";
|
||||
if (!id) return send(res, { error: 1, msg: "missing id" }, 400);
|
||||
|
||||
// 读活动列表,用来补齐标题/时间/地点等字段
|
||||
const actList = readJson("actList.json", {
|
||||
error: 0,
|
||||
data: { list: [] },
|
||||
});
|
||||
const act = (actList.data?.list || []).find(
|
||||
(x) => Number(x.id) === id,
|
||||
);
|
||||
|
||||
// myApplyList.json
|
||||
const applyJson = readJson("myApplyList.json", {
|
||||
error: 0,
|
||||
data: { current: 1, pageSize: 10, pageCount: 1, list: [] },
|
||||
});
|
||||
const list = applyJson.data?.list || [];
|
||||
const existsIdx = list.findIndex((x) => Number(x.id) === id);
|
||||
|
||||
if (action === "cancel") {
|
||||
if (existsIdx >= 0) list.splice(existsIdx, 1);
|
||||
} else {
|
||||
// apply
|
||||
if (existsIdx < 0) {
|
||||
// 申请状态:你的 mock 里 1/2 出现过。这里用 1 表示已申请
|
||||
list.unshift({ ...(act || { id }), applyStatus: 1 });
|
||||
} else {
|
||||
list[existsIdx].applyStatus = 1;
|
||||
}
|
||||
}
|
||||
|
||||
applyJson.data = applyJson.data || {};
|
||||
applyJson.data.list = list;
|
||||
applyJson.data.pageCount = 1;
|
||||
writeJson("myApplyList.json", applyJson);
|
||||
|
||||
// actDetails.json:让详情页刷新后看到申请状态变化
|
||||
const detailsJson = readJson("actDetails.json", {
|
||||
error: 0,
|
||||
data: { details: {} },
|
||||
});
|
||||
if (
|
||||
detailsJson.data?.details &&
|
||||
Number(detailsJson.data.details.id) === id
|
||||
) {
|
||||
if (action === "cancel") {
|
||||
detailsJson.data.details.applyStatus = 0;
|
||||
detailsJson.data.details.canApply = true;
|
||||
} else {
|
||||
detailsJson.data.details.applyStatus = 1;
|
||||
detailsJson.data.details.canApply = false;
|
||||
}
|
||||
writeJson("actDetails.json", detailsJson);
|
||||
}
|
||||
|
||||
// actList.json(可选):列表 canApply 同步变化
|
||||
if (act) {
|
||||
const idx = (actList.data?.list || []).findIndex(
|
||||
(x) => Number(x.id) === id,
|
||||
);
|
||||
if (idx >= 0) {
|
||||
actList.data.list[idx].canApply = action === "cancel";
|
||||
writeJson("actList.json", actList);
|
||||
}
|
||||
}
|
||||
|
||||
return ok(res);
|
||||
} catch {
|
||||
return send(res, { error: 1, msg: "Invalid JSON body" }, 400);
|
||||
}
|
||||
}
|
||||
|
||||
// 3) 上传服务记录:写 serviceList + 联动积分文件
|
||||
if (
|
||||
pathname.startsWith("/api/act/uploadService") &&
|
||||
req.method === "POST"
|
||||
) {
|
||||
try {
|
||||
const incoming = await readBodyJson(req);
|
||||
// 约定:时长字段可能叫 hour / duration / value
|
||||
const hour =
|
||||
Number(
|
||||
incoming.hour ?? incoming.duration ?? incoming.value ?? 0,
|
||||
) || 0;
|
||||
|
||||
// serviceList.json 结构:{data:{list:[{id,pic,content,publisher,time,status}]}}
|
||||
const sl = readJson("serviceList.json", {
|
||||
error: 0,
|
||||
data: { current: 1, pageSize: 10, pageCount: 1, list: [] },
|
||||
});
|
||||
const list = sl.data?.list || [];
|
||||
|
||||
const nextId =
|
||||
list.reduce((m, x) => Math.max(m, Number(x.id) || 0), 0) + 1;
|
||||
|
||||
const imgList = (incoming.fileList || []).map(item => item.url || item.content || incoming.pic || "./imgs/actImg.jpeg");
|
||||
|
||||
list.unshift({
|
||||
id: nextId,
|
||||
pic: incoming.pic || "./imgs/actImg.jpeg",
|
||||
imgList,
|
||||
content: incoming.content || "新增服务记录",
|
||||
publisher: incoming.publisher || "未知来源",
|
||||
time: incoming.time || Date.now(),
|
||||
status: 1, // 1=通过?你 recordDetails.status=1,看起来是已审核/有效
|
||||
// 额外字段可保留(不影响页面)
|
||||
hour,
|
||||
});
|
||||
|
||||
sl.data = sl.data || {};
|
||||
sl.data.list = list;
|
||||
sl.data.pageCount = 1;
|
||||
writeJson("serviceList.json", sl);
|
||||
|
||||
// 同步更新积分(按 1小时=2分)
|
||||
if (hour > 0) addScore(hour);
|
||||
|
||||
return ok(res);
|
||||
} catch {
|
||||
return send(res, { error: 1, msg: "Invalid JSON body" }, 400);
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
base: "./",
|
||||
plugins: [vue(), mockPlugin(), VueDevTools()],
|
||||
});
|
||||
1345
todos/package-lock.json
generated
@@ -9,13 +9,10 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"pinia": "^3.0.4",
|
||||
"sortablejs": "^1.15.6",
|
||||
"vue": "^3.5.24"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"vite": "^7.2.2",
|
||||
"vite-plugin-vue-devtools": "^8.0.5"
|
||||
"vite": "^7.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,66 +1,73 @@
|
||||
<script setup>
|
||||
<script>
|
||||
import TodoHeader from "./components/TodoHeader.vue";
|
||||
import TodoList from "./components/TodoList.vue";
|
||||
import TodoFooter from "./components/TodoFooter.vue";
|
||||
|
||||
import { useTodoStore } from "./stores/todo";
|
||||
import { computed } from "vue";
|
||||
export default {
|
||||
components: {
|
||||
TodoHeader,
|
||||
TodoList,
|
||||
TodoFooter,
|
||||
},
|
||||
|
||||
const todoStore = useTodoStore();
|
||||
data() {
|
||||
return {
|
||||
// 从 localStorage 初始化
|
||||
todos: JSON.parse(localStorage.getItem("vue-todos") || "[]"),
|
||||
tabType: 0, // 0全部 1未完成 2已完成
|
||||
};
|
||||
},
|
||||
|
||||
function addTodo(newTodo) {
|
||||
console.log("App.vue addTodo:", newTodo);
|
||||
todoStore.addTodo(newTodo);
|
||||
}
|
||||
|
||||
function delTodo(item) {
|
||||
console.log("App.vue delTodo:", item);
|
||||
todoStore.delTodo(item);
|
||||
}
|
||||
|
||||
function moveTodo({ oldIndex, newIndex }) {
|
||||
console.log("App.vue moveTodo:", oldIndex, newIndex);
|
||||
todoStore.moveTodo(oldIndex, newIndex);
|
||||
}
|
||||
|
||||
function editTodo({ item, text }) {
|
||||
console.log("App.vue editTodo:", item, text);
|
||||
todoStore.editTodo({ item, text });
|
||||
}
|
||||
|
||||
function toggleTodo(item) {
|
||||
todoStore.toggle(item);
|
||||
}
|
||||
|
||||
function changeTabType(type) {
|
||||
console.log("App.vue tab change:", type);
|
||||
todoStore.setTab(type);
|
||||
}
|
||||
|
||||
const showTodos = computed(() => {
|
||||
if (todoStore.tabType === 1)
|
||||
return todoStore.todos.filter((t) => !t.completed);
|
||||
if (todoStore.tabType === 2)
|
||||
return todoStore.todos.filter((t) => t.completed);
|
||||
return todoStore.todos;
|
||||
computed: {
|
||||
// 筛选后的列表
|
||||
todoList() {
|
||||
if (this.tabType === 0) return this.todos;
|
||||
const type = this.tabType;
|
||||
return this.todos.filter((item) => {
|
||||
if (type === 1) return !item.completed;
|
||||
return item.completed;
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 添加待办
|
||||
addTodo(newTodo) {
|
||||
this.todos.push({
|
||||
id: Date.now(),
|
||||
txt: newTodo,
|
||||
completed: false,
|
||||
});
|
||||
},
|
||||
|
||||
// 删除待办
|
||||
delTodo(item) {
|
||||
this.todos = this.todos.filter((t) => t.id !== item.id);
|
||||
},
|
||||
|
||||
changeTabType(type) {
|
||||
this.tabType = type;
|
||||
},
|
||||
},
|
||||
|
||||
// 深度监听 — 自动写入 localStorage
|
||||
watch: {
|
||||
todos: {
|
||||
handler(todos) {
|
||||
localStorage.setItem("vue-todos", JSON.stringify(todos));
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TodoHeader @addTodo="addTodo" />
|
||||
|
||||
<TodoList
|
||||
:todos="showTodos"
|
||||
@delTodo="delTodo"
|
||||
@move="moveTodo"
|
||||
@edit="editTodo"
|
||||
:toggleTodo="toggleTodo"
|
||||
/>
|
||||
|
||||
<TodoList :todos="todoList" @delTodo="delTodo" />
|
||||
<TodoFooter
|
||||
:count="showTodos.length"
|
||||
:tabType="todoStore.tabType"
|
||||
:count="todoList.length"
|
||||
:tabType="tabType"
|
||||
@changeTabType="changeTabType"
|
||||
@clearCompleted="todoStore.clearCompleted"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
<a :class="{ active: tabType == 1 }" @click="tabClick(1)">未完成</a>
|
||||
<a :class="{ active: tabType == 2 }" @click="tabClick(2)">已完成</a>
|
||||
</div>
|
||||
<button class="clearBtn" @click="$emit('clearCompleted')">清除已完成</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -50,20 +49,8 @@ export default {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.clearBtn {
|
||||
background-color: #ff4d4f;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.clearBtn:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.active {
|
||||
color: #ff4d4f !important;
|
||||
color: #409eff !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -16,36 +16,6 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const WORD_COUNT_LIMIT = 50;
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
newTodo: "",
|
||||
countLimit: WORD_COUNT_LIMIT,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
isShowMsg() {
|
||||
return this.newTodo.length > WORD_COUNT_LIMIT;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
addNewTodo() {
|
||||
if (this.newTodo.length === 0 || this.newTodo.length > WORD_COUNT_LIMIT) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit("addTodo", this.newTodo);
|
||||
this.newTodo = "";
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
h1 {
|
||||
color: #409eff;
|
||||
@@ -87,3 +57,33 @@ p {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const WORD_COUNT_LIMIT = 50;
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
newTodo: "",
|
||||
countLimit: WORD_COUNT_LIMIT,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
isShowMsg() {
|
||||
return this.newTodo.length > WORD_COUNT_LIMIT;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
addNewTodo() {
|
||||
if (this.newTodo.length === 0 || this.newTodo.length > WORD_COUNT_LIMIT) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit("addTodo", this.newTodo);
|
||||
this.newTodo = "";
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,43 +1,13 @@
|
||||
<template>
|
||||
<div class="tdContainer">
|
||||
<ul class="tdList">
|
||||
<li
|
||||
class="tdItem"
|
||||
v-for="(item, index) in todos"
|
||||
:key="item.id"
|
||||
draggable="true"
|
||||
@dragstart="onDragStart(index)"
|
||||
@dragover.prevent="onDragOver(index)"
|
||||
@drop="onDrop(index)"
|
||||
>
|
||||
<li class="tdItem" v-for="item in todos" :key="item.id">
|
||||
<div class="tdItem-main">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="item.completed"
|
||||
@change="toggleTodo(item)"
|
||||
class="toToggle"
|
||||
/>
|
||||
<!-- <span class="tdTxt" :class="{ completed: item.completed }"> -->
|
||||
<!-- {{ item.txt }} -->
|
||||
<!-- </span> -->
|
||||
<span
|
||||
v-if="editingId !== item.id"
|
||||
class="tdTxt"
|
||||
:class="{ completed: item.completed }"
|
||||
@dblclick="startEdit(item)"
|
||||
>
|
||||
<input type="checkbox" v-model="item.completed" class="toToggle" />
|
||||
|
||||
<span class="tdTxt" :class="{ completed: item.completed }">
|
||||
{{ item.txt }}
|
||||
</span>
|
||||
|
||||
<input
|
||||
v-else
|
||||
class="editInput"
|
||||
v-model="editText"
|
||||
@keyup.enter="confirmEdit(item)"
|
||||
@blur="confirmEdit(item)"
|
||||
@keyup.esc="cancelEdit"
|
||||
autofocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="tdItem-acts">
|
||||
@@ -48,49 +18,6 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ["todos", "toggleTodo", "delTodo", "editTodo", "moveTodo"],
|
||||
data() {
|
||||
return {
|
||||
editingId: null,
|
||||
editText: "",
|
||||
dragIndex: null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
delTodo(item) {
|
||||
this.$emit("delTodo", item);
|
||||
},
|
||||
onDragStart(index) {
|
||||
this.dragIndex = index;
|
||||
},
|
||||
onDragOver(index) {},
|
||||
onDrop(index) {
|
||||
if (this.dragIndex === null || this.dragIndex === index) return;
|
||||
this.$emit("move", { oldIndex: this.dragIndex, newIndex: index });
|
||||
this.dragIndex = null;
|
||||
},
|
||||
startEdit(item) {
|
||||
this.editingId = item.id;
|
||||
this.editText = item.txt;
|
||||
},
|
||||
|
||||
confirmEdit(item) {
|
||||
const text = this.editText.trim();
|
||||
if (text) {
|
||||
this.$emit("edit", { item, text });
|
||||
}
|
||||
this.editingId = null;
|
||||
},
|
||||
|
||||
cancelEdit() {
|
||||
this.editingId = null;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tdContainer {
|
||||
margin-top: 20px;
|
||||
@@ -164,11 +91,17 @@ export default {
|
||||
text-decoration: line-through;
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.editInput {
|
||||
font-size: 16px;
|
||||
padding: 4px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ["todos"],
|
||||
|
||||
methods: {
|
||||
delTodo(item) {
|
||||
this.$emit("delTodo", item);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { createApp } from "vue";
|
||||
import { createPinia } from "pinia";
|
||||
import App from "./App.vue";
|
||||
import "./assets/global.css";
|
||||
|
||||
createApp(App).use(createPinia()).mount("#app");
|
||||
createApp(App).mount("#app");
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { toRaw } from "vue";
|
||||
|
||||
const STORAGE_KEY = "vue-todos";
|
||||
|
||||
export const useTodoStore = defineStore("todoStore", {
|
||||
state: () => ({
|
||||
todos: JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]"),
|
||||
tabType: 0, // 0全部 1未完成 2已完成
|
||||
}),
|
||||
|
||||
getters: {
|
||||
leftCount(state) {
|
||||
return state.todos.filter((t) => !t.completed).length;
|
||||
},
|
||||
},
|
||||
|
||||
actions: {
|
||||
save() {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(toRaw(this.todos)));
|
||||
},
|
||||
|
||||
setTab(type) {
|
||||
this.tabType = type;
|
||||
},
|
||||
|
||||
addTodo(txt) {
|
||||
const newTodo = {
|
||||
id: Date.now(),
|
||||
txt,
|
||||
completed: false,
|
||||
};
|
||||
this.todos.push(newTodo);
|
||||
this.save();
|
||||
console.log("Added:", newTodo);
|
||||
console.log("Current todos:", toRaw(this.todos));
|
||||
},
|
||||
|
||||
delTodo(item) {
|
||||
this.todos = this.todos.filter((t) => t.id !== item.id);
|
||||
this.save();
|
||||
console.log("Deleted:", item);
|
||||
console.log("Current todos:", toRaw(this.todos));
|
||||
},
|
||||
|
||||
editTodo({ item, text }) {
|
||||
const t = this.todos.find((t) => t.id === item.id);
|
||||
if (t) {
|
||||
t.txt = text;
|
||||
this.save();
|
||||
console.log("Edited:", item.id, "=>", text);
|
||||
console.log("Current todos:", toRaw(this.todos));
|
||||
}
|
||||
},
|
||||
|
||||
moveTodo(oldIndex, newIndex) {
|
||||
if (oldIndex === newIndex) return;
|
||||
|
||||
const arr = this.todos;
|
||||
const moved = arr.splice(oldIndex, 1)[0];
|
||||
arr.splice(newIndex, 0, moved);
|
||||
|
||||
this.save();
|
||||
console.log(`Move: ${oldIndex} → ${newIndex}`);
|
||||
console.log("Current todos:", toRaw(this.todos));
|
||||
},
|
||||
|
||||
toggle(item) {
|
||||
const t = this.todos.find(t => t.id === item.id);
|
||||
if (t) {
|
||||
t.completed = !t.completed;
|
||||
this.save();
|
||||
console.log("Toggled:", t);
|
||||
}
|
||||
},
|
||||
|
||||
clearCompleted() {
|
||||
this.todos = this.todos.filter((t) => !t.completed);
|
||||
this.save();
|
||||
console.log("Clear completed");
|
||||
console.log("Current todos:", toRaw(this.todos));
|
||||
},
|
||||
},
|
||||
});
|
||||
BIN
todos/view.png
|
Before Width: | Height: | Size: 94 KiB |
@@ -1,8 +1,8 @@
|
||||
import { defineConfig } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import VueDevTools from "vite-plugin-vue-devtools";
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
base: "./",
|
||||
plugins: [vue(), VueDevTools()],
|
||||
base: './',
|
||||
plugins: [vue()],
|
||||
});
|
||||
|
||||