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

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