优化登录与任务管理相关页面交互

This commit is contained in:
2026-04-12 13:05:00 +08:00
parent 5c0f7e932f
commit cef3192ba5
4 changed files with 469 additions and 283 deletions
+154 -58
View File
@@ -1,36 +1,27 @@
<script setup lang="ts"> <script setup lang="ts">
import {reactive, ref} from "vue"; import { reactive, ref } from "vue";
import {ElMessage, type FormInstance, type FormRules} from "element-plus"; import { ElMessage, type FormInstance, type FormRules } from "element-plus";
import type {InternalRuleItem, Value} from "async-validator"; import type { InternalRuleItem, Value } from "async-validator";
import router from "@/router"; import router from "@/router";
import { login } from "@/api/login"; import { login } from "@/api/login";
interface RegisterData { interface RegisterData {
username: string username: string;
password: string password: string;
} }
const registerData = reactive<RegisterData>({ const registerData = reactive<RegisterData>({
username: "", username: "",
password: "" password: "",
}); });
const loginForm = ref<FormInstance>(); const loginForm = ref<FormInstance>();
const loading = ref(false);
const rules = reactive<FormRules<RegisterData>>({ const rules = reactive<FormRules<RegisterData>>({
password: [ password: [{ validator: isNotEmpty, message: "请输入密码!" }],
{ username: [{ validator: isNotEmpty, message: "请输入账号!" }],
validator: isNotEmpty, });
message:"请输入密码!"
}
],
username: [
{
validator: isNotEmpty,
message:"请输入账号!"
}
]
}
)
function isNotEmpty(rule: InternalRuleItem, value: Value, callback: any) { function isNotEmpty(rule: InternalRuleItem, value: Value, callback: any) {
if (!value) { if (!value) {
@@ -41,52 +32,157 @@ function isNotEmpty(rule: InternalRuleItem, value: Value, callback: any) {
} }
const submitForm = async (formEl: FormInstance | undefined) => { const submitForm = async (formEl: FormInstance | undefined) => {
if (!formEl) return if (!formEl || loading.value) return;
await formEl.validate((valid, fields) => { await formEl.validate(async (valid) => {
if (valid) { if (!valid) return;
login(registerData.username, registerData.password).then(result =>{ loading.value = true;
if (result.code === 200) { try {
localStorage.setItem("isLoggedIn", "true"); const result = await login(registerData.username, registerData.password);
ElMessage.success(result.message) if (result.code === 200) {
router.push("/welcome") localStorage.setItem("isLoggedIn", "true");
} else { ElMessage.success(result.message);
ElMessage.error(result.message) await router.push("/welcome");
} } else {
}) ElMessage.error(result.message);
} else { }
console.log('error submit!', fields) } finally {
loading.value = false;
} }
}) });
} };
</script> </script>
<template> <template>
<div class="login"> <section class="login-page">
<el-form ref="loginForm" :model = "registerData"> <div class="login-panel surface-card">
<el-form-item label="账号:" :rules="rules.username" prop="username"> <div class="hero">
<el-input v-model= "registerData.username"/> <p class="hero-kicker">LPT · Focus Flow</p>
</el-form-item> <h1>今天的学习从一次清晰登录开始</h1>
<el-form-item label="密码:" :rules="rules.password" prop="password"> <p>
<el-input v-model= "registerData.password" type="password"/> 你将看到任务优先级学习会话和复习数据在同一个系统里自然衔接
</el-form-item> </p>
<el-form-item> <ul class="hero-points">
<el-button type="primary" @click="submitForm(loginForm)" style="width: 100%;">登录</el-button> <li>明确学习任务</li>
</el-form-item> <li>记录会话与残片</li>
</el-form> <li>持续复盘</li>
</div> </ul>
</div>
<div class="form-area">
<h2>账号登录</h2>
<el-form ref="loginForm" :model="registerData" class="login-form">
<el-form-item label="账号" :rules="rules.username" prop="username">
<el-input
v-model="registerData.username"
placeholder="请输入账号"
@keyup.enter="submitForm(loginForm)"
/>
</el-form-item>
<el-form-item label="密码" :rules="rules.password" prop="password">
<el-input
v-model="registerData.password"
type="password"
show-password
placeholder="请输入密码"
@keyup.enter="submitForm(loginForm)"
/>
</el-form-item>
<el-button
type="success"
class="submit-btn"
:loading="loading"
@click="submitForm(loginForm)"
>
进入系统
</el-button>
</el-form>
</div>
</div>
</section>
</template> </template>
<style scoped> <style scoped>
*{ .login-page {
margin: 0; min-height: calc(100vh - 240px);
padding: 0; display: flex;
align-items: center;
justify-content: center;
} }
.login { .login-panel {
min-height: 25vh; width: min(980px, 100%);
min-width: 30vw; display: grid;
background: #22b9fa; grid-template-columns: 1.1fr 1fr;
gap: 0;
overflow: hidden;
}
.hero {
padding: 36px;
background: linear-gradient(150deg, var(--green-800) 0%, var(--green-600) 100%);
color: #effcf4;
}
.hero-kicker {
margin: 0;
font-size: 13px;
letter-spacing: 1.4px;
opacity: 0.85;
}
.hero h1 {
margin: 14px 0 10px;
line-height: 1.35;
font-size: clamp(22px, 2.6vw, 32px);
}
.hero p {
margin: 0;
opacity: 0.88;
}
.hero-points {
margin: 20px 0 0;
padding-left: 18px;
}
.hero-points li {
margin-top: 6px;
}
.form-area {
background: var(--surface-strong);
padding: 32px 30px;
}
.form-area h2 {
margin: 0 0 16px;
color: var(--green-900);
}
.login-form :deep(.el-form-item) {
margin-bottom: 16px;
}
.submit-btn {
width: 100%;
margin-top: 8px;
}
@media (max-width: 900px) {
.login-panel {
grid-template-columns: 1fr;
}
}
@media (max-width: 768px) {
.login-page {
min-height: auto;
}
.hero,
.form-area {
padding: 22px 16px;
}
} }
</style> </style>
+85 -16
View File
@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref } from "vue"; import { computed, onMounted, ref } from "vue";
import request from "@/utils/request"; import request from "@/utils/request";
interface ReviewTask { interface ReviewTask {
@@ -13,6 +13,12 @@ interface ReviewTask {
const loading = ref(false); const loading = ref(false);
const tasks = ref<ReviewTask[]>([]); const tasks = ref<ReviewTask[]>([]);
const summary = computed(() => ({
totalTasks: tasks.value.length,
totalReports: tasks.value.reduce((acc, item) => acc + Number(item.reportCount || 0), 0),
totalFragments: tasks.value.reduce((acc, item) => acc + Number(item.fragmentCount || 0), 0),
}));
const loadTasks = async () => { const loadTasks = async () => {
loading.value = true; loading.value = true;
try { try {
@@ -35,27 +41,90 @@ onMounted(() => {
</script> </script>
<template> <template>
<el-card> <section class="review-page">
<template #header> <header class="surface-card review-head">
<div class="header"> <div>
<span>复习任务总览</span> <h1 class="page-title">复习总览</h1>
<el-button type="primary" plain @click="loadTasks">刷新</el-button> <p class="page-subtitle">根据学习报告与残片数量快速识别需要回顾的任务</p>
</div> </div>
</template> <el-button type="success" plain :loading="loading" @click="loadTasks">刷新</el-button>
</header>
<el-table v-loading="loading" :data="tasks" stripe> <div class="summary-grid">
<el-table-column prop="taskName" label="任务名" min-width="180" /> <article class="surface-card metric-card">
<el-table-column prop="effectiveTime" label="累计有效学习时长" min-width="140" /> <span>任务数</span>
<el-table-column prop="reportCount" label="学习报告数" min-width="120" /> <strong>{{ summary.totalTasks }}</strong>
<el-table-column prop="fragmentCount" label="学习残片数" min-width="120" /> </article>
</el-table> <article class="surface-card metric-card">
</el-card> <span>学习报告总数</span>
<strong>{{ summary.totalReports }}</strong>
</article>
<article class="surface-card metric-card">
<span>学习残片总数</span>
<strong>{{ summary.totalFragments }}</strong>
</article>
</div>
<article class="surface-card table-card">
<el-table v-loading="loading" :data="tasks" stripe>
<el-table-column prop="taskName" label="任务名" min-width="180" />
<el-table-column prop="effectiveTime" label="累计有效学习时长" min-width="150" />
<el-table-column prop="reportCount" label="学习报告数" min-width="120" />
<el-table-column prop="fragmentCount" label="学习残片数" min-width="120" />
</el-table>
</article>
</section>
</template> </template>
<style scoped> <style scoped>
.header { .review-page {
display: flex; display: flex;
align-items: center; flex-direction: column;
gap: 14px;
}
.review-head {
padding: 20px 22px;
display: flex;
align-items: flex-start;
justify-content: space-between; justify-content: space-between;
gap: 12px;
}
.summary-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
}
.metric-card {
padding: 16px 18px;
}
.metric-card span {
color: var(--text-secondary);
font-size: 13px;
}
.metric-card strong {
display: block;
margin-top: 6px;
font-size: 30px;
line-height: 1;
color: var(--green-900);
}
.table-card {
padding: 10px;
}
@media (max-width: 900px) {
.summary-grid {
grid-template-columns: 1fr;
}
.review-head {
flex-direction: column;
}
} }
</style> </style>
+139 -133
View File
@@ -1,19 +1,18 @@
<script setup lang="ts"> <script setup lang="ts">
import {computed, onMounted, onUnmounted, ref} from 'vue'; import { computed, onMounted, onUnmounted, ref } from "vue";
import request from "@/utils/request"; import request from "@/utils/request";
import {ElMessage} from "element-plus"; import { ElMessage } from "element-plus";
import {useRoute, useRouter} from "vue-router"; import { useRoute, useRouter } from "vue-router";
const router = useRouter(); const router = useRouter();
const route = useRoute();
const route = useRoute()
const taskId = route.params.taskId ? Number(route.params.taskId) : null; const taskId = route.params.taskId ? Number(route.params.taskId) : null;
const buttonName = route.meta.buttonText; const buttonName = route.meta.buttonText as string;
const taskName = ref("");
const taskName = ref(''); const taskDescription = ref("");
const taskDescription = ref(''); const materialURL = ref("");
const materialURL = ref('');
const priority = ref({ const priority = ref({
urgency: 0, urgency: 0,
importance: 0, importance: 0,
@@ -23,33 +22,34 @@ const priority = ref({
}); });
const priorityOptions = [0, 1, 2, 3, 4, 5]; const priorityOptions = [0, 1, 2, 3, 4, 5];
const pageTitle = computed(() => (route.name === "update-task" ? "更新学习任务" : "创建学习任务"));
const pageSubtitle = computed(() =>
route.name === "update-task"
? "修改任务信息后保存,后续学习会话将使用最新内容"
: "填写任务基本信息与优先级维度,帮助系统更好排序"
);
const createTask = () => { const createTask = () => {
console.log('创建任务', { request
taskName: taskName.value, .post("/tasks", {
taskDescription: taskDescription.value, taskName: taskName.value,
priority: priority.value, taskDescription: taskDescription.value,
}); materialURL: materialURL.value,
...priority.value,
request.post("/tasks", { })
taskName: taskName.value, .then((result) => {
taskDescription: taskDescription.value, if (result.code === 200) {
materialURL: materialURL.value, ElMessage.success("创建任务成功");
...priority.value router.back();
}).then(result => { } else {
if (result.code == 200) { ElMessage.error("任务创建失败:" + result.message);
ElMessage.success("创建任务成功,任务id" + result.message) }
router.back() });
} else {
ElMessage.error("任务创建失败,错误原因:" + result.message)
}
})
}; };
const loadTask = () => { const loadTask = () => {
console.info("开始加载任务信息")
if (taskId !== null) { if (taskId !== null) {
request.get(`/tasks/${taskId}`, {}).then(result => { request.get(`/tasks/${taskId}`, {}).then((result) => {
if (result.code === 200) { if (result.code === 200) {
const data = result.data; const data = result.data;
taskName.value = data.taskName; taskName.value = data.taskName;
@@ -70,46 +70,41 @@ const loadTask = () => {
}; };
const updateTask = () => { const updateTask = () => {
console.log('更新任务', { request
taskName: taskName.value, .put("/tasks/" + taskId, {
taskDescription: taskDescription.value, id: taskId,
priority: priority.value, taskName: taskName.value,
}); taskDescription: taskDescription.value,
materialURL: materialURL.value,
request.put("/tasks/" + taskId, { ...priority.value,
id: taskId, })
taskName: taskName.value, .then((result) => {
taskDescription: taskDescription.value, if (result.code === 200) {
materialURL: materialURL.value, ElMessage.success("更新任务成功");
...priority.value router.back();
}).then(result => { } else {
if (result.code == 200) { ElMessage.error("任务更新失败:" + result.message);
ElMessage.success("更新任务成功" + result.message) }
router.back() });
} else {
ElMessage.error("任务更新失败,错误原因:" + result.message)
}
})
}; };
const cancelTask = () => { const cancelTask = () => {
ElMessage.success("已取消创建") ElMessage.info("已取消操作");
router.back() router.back();
}; };
const submitTask = () => { const submitTask = () => {
switch (route.name) { switch (route.name) {
case 'add-task': case "add-task":
createTask(); createTask();
break; break;
case 'update-task': case "update-task":
updateTask(); updateTask();
break; break;
default: default:
ElMessage.error('未知操作类型'); ElMessage.error("未知操作类型");
} }
} };
const screenWidth = ref(window.innerWidth); const screenWidth = ref(window.innerWidth);
@@ -117,123 +112,134 @@ const updateWidth = () => {
screenWidth.value = window.innerWidth; screenWidth.value = window.innerWidth;
}; };
onMounted(() => { onMounted(() => {
window.addEventListener('resize', updateWidth) window.addEventListener("resize", updateWidth);
if (route.meta.action === 'update' && taskId !== null) { if (route.meta.action === "update" && taskId !== null) {
loadTask(); loadTask();
} }
}); });
onUnmounted(() => window.removeEventListener('resize', updateWidth));
const columnSpan = computed(() => { onUnmounted(() => window.removeEventListener("resize", updateWidth));
return screenWidth.value <= 768 ? 20 : 8;
}); const isMobile = computed(() => screenWidth.value <= 900);
</script> </script>
<template> <template>
<el-form label-width="100px"> <section class="task-form-page">
<el-form-item label="任务名:"> <header class="surface-card form-header">
<el-input v-model="taskName" placeholder="请输入任务名"></el-input> <h1 class="page-title">{{ pageTitle }}</h1>
</el-form-item> <p class="page-subtitle">{{ pageSubtitle }}</p>
</header>
<el-form-item label="任务描述:"> <el-form label-position="top" class="surface-card form-shell">
<el-input <div class="group">
v-model="taskDescription" <h3>基础信息</h3>
type="textarea" <el-form-item label="任务名称">
placeholder="请输入任务描述" <el-input v-model="taskName" placeholder="例如:Vue3 组件通信实践" />
rows="6" </el-form-item>
></el-input> <el-form-item label="任务描述">
</el-form-item> <el-input v-model="taskDescription" type="textarea" :rows="5" placeholder="请输入任务描述" />
</el-form-item>
<el-form-item label="学习材料地址">
<el-input v-model="materialURL" placeholder="例如:https://..." />
</el-form-item>
</div>
<el-form-item label="材料地址:"> <div class="group">
<el-input v-model="materialURL" placeholder="请输入学习材料URL"></el-input> <h3>优先级维度</h3>
</el-form-item> <div class="priority-grid" :class="{ mobile: isMobile }">
<el-form-item label="优先级:">
<el-row :gutter="10">
<el-col :span="columnSpan">
<el-form-item label="急迫性"> <el-form-item label="急迫性">
<el-select v-model="priority.urgency"> <el-select v-model="priority.urgency">
<el-option <el-option v-for="item in priorityOptions" :key="'u' + item" :label="item" :value="item" />
v-for="item in priorityOptions"
:key="item"
:label="item"
:value="item"
></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col>
<el-col :span="columnSpan">
<el-form-item label="重要性"> <el-form-item label="重要性">
<el-select v-model="priority.importance"> <el-select v-model="priority.importance">
<el-option <el-option v-for="item in priorityOptions" :key="'i' + item" :label="item" :value="item" />
v-for="item in priorityOptions"
:key="item"
:label="item"
:value="item"
></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col>
<el-col :span="columnSpan">
<el-form-item label="内容难度"> <el-form-item label="内容难度">
<el-select v-model="priority.contentDifficulty"> <el-select v-model="priority.contentDifficulty">
<el-option <el-option v-for="item in priorityOptions" :key="'d' + item" :label="item" :value="item" />
v-for="item in priorityOptions"
:key="item"
:label="item"
:value="item"
></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col>
<el-col :span="columnSpan">
<el-form-item label="未来价值"> <el-form-item label="未来价值">
<el-select v-model="priority.futureValue"> <el-select v-model="priority.futureValue">
<el-option <el-option v-for="item in priorityOptions" :key="'f' + item" :label="item" :value="item" />
v-for="item in priorityOptions"
:key="item"
:label="item"
:value="item"
></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col>
<el-col :span="columnSpan">
<el-form-item label="主观优先级"> <el-form-item label="主观优先级">
<el-select v-model="priority.subjectivePriority"> <el-select v-model="priority.subjectivePriority">
<el-option <el-option v-for="item in priorityOptions" :key="'s' + item" :label="item" :value="item" />
v-for="item in priorityOptions"
:key="item"
:label="item"
:value="item"
></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col> </div>
</el-row> </div>
</el-form-item>
<el-form-item> <div class="action-row">
<el-button type="primary" @click="submitTask">{{ buttonName }}</el-button> <el-button type="success" size="large" @click="submitTask">{{ buttonName }}</el-button>
<el-button @click="cancelTask">取消</el-button> <el-button size="large" @click="cancelTask">取消</el-button>
</el-form-item> </div>
</el-form> </el-form>
</section>
</template> </template>
<style scoped> <style scoped>
.el-form-item { .task-form-page {
margin-bottom: 15px; display: flex;
flex-direction: column;
gap: 14px;
}
.form-header {
padding: 20px 22px;
}
.form-shell {
padding: 22px;
}
.group + .group {
margin-top: 12px;
}
.group h3 {
margin: 0 0 12px;
color: var(--green-900);
}
.priority-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0 14px;
}
.priority-grid.mobile {
grid-template-columns: 1fr;
}
.action-row {
margin-top: 8px;
display: flex;
gap: 10px;
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.el-col { .form-shell {
width: 100% !important; padding: 16px;
}
.action-row {
flex-direction: column;
}
.action-row .el-button {
width: 100%;
margin: 0;
} }
} }
</style> </style>
+91 -76
View File
@@ -9,98 +9,113 @@ const startReview = () => {
router.push("/review"); router.push("/review");
}; };
</script> </script>
<template> <template>
<el-container class="container"> <section class="welcome-page">
<el-header class="header"> <div class="welcome-banner surface-card">
欢迎回来今天也稳步推进学习计划 <div>
</el-header> <h1 class="page-title">欢迎回来准备好继续学习了吗</h1>
<p class="page-subtitle">
先挑选任务开始一次会话再在复习页回顾你的学习轨迹
</p>
</div>
<el-tag type="success" effect="dark" size="large">今日模式稳步推进</el-tag>
</div>
<div class="quick-grid">
<article class="quick-card surface-card">
<h3>学习会话</h3>
<p>进入任务列表选择当前最重要的一项并开始学习计时</p>
<el-button type="success" size="large" @click="startStudy">开始学习</el-button>
</article>
<el-main> <article class="quick-card surface-card">
<el-row :gutter="20"> <h3>复习回看</h3>
<el-col :span="18"> <p>查看学习报告与残片数量明确哪些内容需要优先复习</p>
<el-card class="progress-card" shadow="always"> <el-button plain type="success" size="large" @click="startReview">开始复习</el-button>
<div class="progress-text">学习进度总揽</div> </article>
</el-card> </div>
</el-col>
<el-col :span="3"> <div class="tips-board surface-card">
<div class="button-group"> <h3>建议流程</h3>
<el-button type="success" size="large" @click="startStudy" plain>开始学习</el-button> <ol>
<el-button type="primary" size="large" @click="startReview" plain>开始复习</el-button> <li>进入学习页确认任务材料地址与优先级</li>
</div> <li>开始会话并在休息时记录学习残片</li>
</el-col> <li>结束会话后填写总结再回到复习页检视数据</li>
</el-row> </ol>
</el-main> </div>
</el-container> </section>
</template> </template>
<style scoped> <style scoped>
*{ .welcome-page {
margin: 0px;
/*padding: 0;*/
}
.container {
min-height: 100vh;
font-family: "Helvetica Neue", Arial, sans-serif;
}
.header {
text-align: center;
font-size: 16px;
font-weight: bold;
border-bottom: 1px solid #ebeef5;
padding: 20px;
}
.progress-card {
height: 400px;
display: flex;
align-items: center;
justify-content: center;
}
.progress-text {
font-size: 18px;
color: #333;
}
.button-group {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 20px; gap: 16px;
margin-top: 80px;
align-items: center;
}
.button-group .el-button {
margin-left: 0;
} }
.welcome-banner {
/*滚动样式*/ padding: 20px 22px;
.marquee-wrapper { display: flex;
overflow: hidden; align-items: flex-start;
white-space: nowrap; justify-content: space-between;
position: relative; gap: 16px;
height: 40px;
} }
.marquee-content { .quick-grid {
display: inline-block; display: grid;
position: absolute; grid-template-columns: repeat(2, minmax(0, 1fr));
white-space: nowrap; gap: 16px;
will-change: transform;
animation: scroll-left 15s linear infinite;
font-size: 16px;
font-weight: bold;
} }
@keyframes scroll-left { .quick-card {
0% { padding: 22px;
transform: translateX(100%); display: flex;
flex-direction: column;
gap: 10px;
}
.quick-card h3 {
margin: 0;
color: var(--green-900);
}
.quick-card p {
margin: 0;
color: var(--text-secondary);
}
.quick-card .el-button {
margin-top: auto;
width: 100%;
}
.tips-board {
padding: 20px 22px;
}
.tips-board h3 {
margin: 0;
color: var(--green-900);
}
.tips-board ol {
margin: 10px 0 0;
padding-left: 20px;
color: var(--text-secondary);
}
.tips-board li + li {
margin-top: 8px;
}
@media (max-width: 900px) {
.welcome-banner {
flex-direction: column;
} }
100% {
transform: translateX(-100%); .quick-grid {
grid-template-columns: 1fr;
} }
} }
</style> </style>