feat:教师用户页面

This commit is contained in:
JiXinHui 2025-07-04 16:04:01 +08:00
parent abc79e407a
commit f71f3899f1
30 changed files with 1019 additions and 6 deletions

94
components/PageHeader.vue Normal file
View File

@ -0,0 +1,94 @@
<template>
<div class="page-header">
<u-navbar
:title="title"
:is-back="isBack"
:back-text="backText"
:border-bottom="borderBottom"
:background="background"
:title-color="titleColor"
:title-size="titleSize"
:title-bold="titleBold"
:z-index="zIndex"
@leftClick="onBackClick"
>
<template v-slot:right>
<slot name="right"></slot>
</template>
<slot></slot>
</u-navbar>
</div>
</template>
<script>
export default {
name: 'PageHeader',
props: {
//
title: {
type: String,
default: ''
},
//
isBack: {
type: Boolean,
default: true
},
//
backText: {
type: String,
default: ''
},
//
borderBottom: {
type: Boolean,
default: true
},
//
background: {
type: Object,
default() {
return {
background: '#ffffff'
}
}
},
//
titleColor: {
type: String,
default: '#191919'
},
//
titleSize: {
type: [Number, String],
default: 34
},
//
titleBold: {
type: Boolean,
default: true
},
// z-index
zIndex: {
type: [Number, String],
default: 980
},
},
methods: {
//
onBackClick() {
this.$emit('back');
},
//
switchTab(tabValue) {
this.$emit('tab-change', tabValue);
}
}
}
</script>
<style scoped>
.page-header {
width: 100%;
}
</style>

110
components/TabBar.vue Normal file
View File

@ -0,0 +1,110 @@
<template>
<u-tabbar
:value="currentIndex"
:show="true"
:bg-color="'#fff'"
:border-top="false"
:fixed="true"
:height="148"
:list="formattedTabList"
@change="switchTab"
>
</u-tabbar>
</template>
<script>
export default {
name: "TabBar",
props: {
currentPath: {
type: String,
default: "",
},
},
data() {
return {
currentIndex: 0,
tabList: [
{
text: "会话列表",
iconText: "📋",
useImg: true,
icon: "/static/tabbar/tabbar-icon1.png",
activeIcon: "/static/tabbar/tabbar-icon1.png",
pagePath: "/pages/home/index",
},
{
text: "留言板",
iconText: "📊",
useImg: true,
icon: "/static/tabbar/tabbar-icon2.png",
activeIcon: "/static/tabbar/tabbar-icon2.png",
pagePath: "/pages/notes/index",
},
{
text: "我的",
iconText: "👤",
useImg: true,
icon: "/static/tabbar/tabbar-icon3.png",
activeIcon: "/static/tabbar/tabbar-icon3.png",
pagePath: "/pages/my/index",
},
],
};
},
computed: {
// tabListuviewu-tabbar
formattedTabList() {
return this.tabList.map((item) => {
return {
text: item.text,
iconPath: item.icon,
selectedIconPath: item.activeIcon,
pagePath: item.pagePath,
};
});
},
},
created() {
// tab
this.setActiveTab();
},
methods: {
setActiveTab() {
const currentPath = this.currentPath || this.getCurrentPath();
this.tabList.forEach((item, index) => {
if (currentPath.includes(item.pagePath)) {
this.currentIndex = index;
}
});
},
getCurrentPath() {
//
const pages = getCurrentPages();
if (pages.length > 0) {
const currentPage = pages[pages.length - 1];
return "/" + currentPage.route;
}
return "";
},
switchTab(index) {
if (this.currentIndex === index) return;
this.currentIndex = index;
this.$emit("change", this.tabList[index].pagePath, index);
//
uni.switchTab({
url: this.tabList[index].pagePath,
});
},
},
};
</script>
<style scoped>
/* /deep/.u-icon__img {
width: 54rpx !important;
height: 54rpx !important;
} */
</style>

View File

@ -13,10 +13,23 @@
},
"pages": [
{
"path": "pages/home/index/index",
"path": "pages/home/index",
"style": {
"navigationBarTitleText": "首页",
"enablePullDownRefresh": false,
"navigationBarTitleText": "会话列表",
"navigationStyle": "custom"
}
},
{
"path": "pages/notes/index",
"style": {
"navigationBarTitleText": "留言板",
"navigationStyle": "custom"
}
},
{
"path": "pages/my/index",
"style": {
"navigationBarTitleText": "我的",
"navigationStyle": "custom"
}
},
@ -152,8 +165,34 @@
"subPackages": [],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "应行AI",
"navigationBarBackgroundColor": "#FFFFFF",
"backgroundColor": "#FFFFFF"
"navigationBarTitleText": "英星AI",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8"
},
"tabBar": {
"color": "#999999",
"selectedColor": "#4a6cf7",
"borderStyle": "black",
"backgroundColor": "#ffffff",
"list": [
{
"pagePath": "pages/home/index",
"iconPath": "static/tabbar/icon_home.png",
"selectedIconPath": "static/tabbar/icon_home_active.png",
"text": "会话列表"
},
{
"pagePath": "pages/notes/index",
"iconPath": "static/tabbar/icon_message.png",
"selectedIconPath": "static/tabbar/icon_message_active.png",
"text": "留言板"
},
{
"pagePath": "pages/my/index",
"iconPath": "static/tabbar/icon_my.png",
"selectedIconPath": "static/tabbar/icon_my_active.png",
"text": "我的"
}
]
}
}

71
pages/home/index.vue Normal file
View File

@ -0,0 +1,71 @@
<template>
<div class="home-page">
<!-- 使用PageHeader组件 -->
<PageHeader
title="会话列表"
:is-back="false"
:border-bottom="true"
:background="headerBackground"
/>
<div class="content-wrapper">
<!-- 这里是会话列表内容 -->
<div class="placeholder">会话列表内容区域</div>
</div>
<!-- 使用TabBar组件 -->
<TabBar :currentPath="'/pages/home/index'" @change="handleTabChange" />
</div>
</template>
<script>
import TabBar from '@/components/TabBar.vue';
import PageHeader from '@/components/PageHeader.vue';
export default {
name: 'HomePage',
components: {
TabBar,
PageHeader
},
data() {
return {
headerBackground: {
background: 'linear-gradient(to right, #e0eafc, #cfdef3)'
}
};
},
methods: {
handleTabChange(path, index) {
console.log('切换到标签页:', path, index);
}
}
}
</script>
<style scoped>
.home-page {
height: 100vh;
background-color: #f5f6fa;
display: flex;
flex-direction: column;
position: relative;
}
.content-wrapper {
flex: 1;
display: flex;
flex-direction: column;
padding: 15px;
padding-bottom: 60px; /* 为底部导航栏预留空间 */
overflow: hidden; /* 防止滚动条出现 */
}
.placeholder {
background-color: #fff;
border-radius: 8px;
padding: 20px;
text-align: center;
color: #999;
}
</style>

249
pages/my/index.vue Normal file
View File

@ -0,0 +1,249 @@
<template>
<div class="my-page">
<PageHeader
title="我的"
:is-back="false"
:border-bottom="false"
:background="headerBackground"
/>
<div class="content-wrapper">
<div class="user-info">
<div class="avatar">
<!-- <img src="" alt="用户头像" /> -->
</div>
<div class="info">
<div class="name">孙老师</div>
<div class="tag">
<image
class="tag-icon"
src="@/static/notes/collage-icon.png"
></image>
江西师范学科技职业学院
</div>
<div class="tag">
<image class="tag-icon" src="@/static/notes/major-icon.png"></image>
生命科学与工程学院 工业工程专业
</div>
</div>
</div>
<div class="statistics">
<div class="stat-item">
<div class="stat-num">36</div>
<div class="stat-label">总答题</div>
</div>
<div class="stat-item">
<div class="stat-num">10</div>
<div class="stat-label">已完成</div>
</div>
<div class="stat-item">
<div class="stat-num">26</div>
<div class="stat-label">未回复</div>
</div>
</div>
<div class="banner">
<img src="@/static/notes/banner.png" alt="banner" />
</div>
<div class="menu-list">
<div class="menu-item" @click="navigateTo('personal-info')">
<div class="menu-icon">
<image src="@/static/notes/menu1.png" class="menu-icon-img"></image>
</div>
<div class="menu-text">个人信息</div>
<view class="arrow-icon">
<u-icon name="arrow-right" color="#999" size="24"></u-icon>
</view>
</div>
<div class="menu-item" @click="navigateTo('change-password')">
<div class="menu-icon">
<image src="@/static/notes/menu2.png" class="menu-icon-img"></image>
</div>
<div class="menu-text">修改密码</div>
<view class="arrow-icon">
<u-icon name="arrow-right" color="#999" size="24"></u-icon>
</view>
</div>
<div class="menu-item" @click="navigateTo('logout-records')">
<div class="menu-icon">
<image src="@/static/notes/menu3.png" class="menu-icon-img"></image>
</div>
<div class="menu-text">退出登录</div>
<view class="arrow-icon">
<u-icon name="arrow-right" color="#999" size="24"></u-icon>
</view>
</div>
</div>
</div>
<TabBar :currentPath="'/pages/my/index'" @change="handleTabChange" />
</div>
</template>
<script>
import TabBar from "@/components/TabBar.vue";
import PageHeader from "@/components/PageHeader.vue";
export default {
name: "MyPage",
components: {
TabBar,
PageHeader,
},
data() {
return {
headerBackground: {
background: "transparent",
},
};
},
methods: {
navigateTo(route) {
//
console.log("导航到:", route);
},
handleTabChange(path, index) {
console.log("切换到标签页:", path, index);
},
},
};
</script>
<style scoped>
.my-page {
height: 100vh;
/* background-color: #f5f6fa; */
background-image: url("@/static/notes/bg.png");
background-position: center top;
background-repeat: no-repeat;
background-size: 100% auto; /* 以宽度为基准等比例缩放 */
display: flex;
flex-direction: column;
position: relative;
}
.content-wrapper {
flex: 1;
display: flex;
flex-direction: column;
padding-bottom: 60px; /* 为底部导航栏预留空间 */
overflow: hidden; /* 防止滚动条出现 */
}
.user-info {
display: flex;
padding: 30rpx;
}
.avatar {
width: 148rpx;
height: 148rpx;
border-radius: 16rpx;
overflow: hidden;
margin-right: 30rpx;
background-color: #ddd;
}
.avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.info .name {
font-size: 16px;
font-weight: bold;
margin-bottom: 24rpx;
}
.tag-icon {
width: 20rpx;
height: 20rpx;
margin-right: 16rpx;
}
.tag {
font-size: 10px;
font-weight: 500;
color: #666;
margin-bottom: 26rpx;
display: flex;
align-items: center;
}
.statistics {
display: flex;
justify-content: space-around;
/* margin-top: 5px;
padding: 15px 0; */
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 20rpx 0 26rpx 0;
}
.stat-num {
font-size: 20px;
font-weight: bold;
line-height: 40rpx;
color: #333;
}
.stat-label {
font-size: 10px;
color: #666;
margin-top: 16rpx;
}
.banner {
padding: 0 26rpx 0 26rpx;
}
.banner img {
width: 100%;
border-radius: 8px;
height: 100%;
object-fit: cover;
}
.menu-list {
background-color: #fff;
border-radius: 8px;
margin: 54rpx 30rpx 0 30rpx;
}
.menu-item {
display: flex;
align-items: center;
padding: 15px;
}
.menu-icon {
margin-right: 10px;
font-size: 20px;
}
.menu-text {
flex: 1;
font-size: 16px;
}
.arrow-icon {
display: flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
}
.menu-icon-img {
width: 28rpx;
height: 28rpx;
}
</style>

449
pages/notes/index.vue Normal file
View File

@ -0,0 +1,449 @@
<template>
<div class="notes-page">
<div class="header">
<div class="title">留言板</div>
<!-- 顶部切换标签 -->
<div class="tab-header">
<div
class="tab-item"
:class="{ active: activeTab === 'unread' }"
@click="switchTab('unread')"
>
未回复
</div>
<div
class="tab-item"
:class="{ active: activeTab === 'replied' }"
@click="switchTab('replied')"
>
已回复
</div>
</div>
</div>
<!-- 内容区域包装器 -->
<div class="content-wrapper">
<!-- 留言列表 -->
<scroll-view class="message-list" scroll-y>
<view v-for="(item, index) in currentMessages" :key="index">
<u-swipe-action
:options="swipeOptions"
:show="item.show"
:index="index"
@click="handleSwipeClick"
@open="e => handleSwipeOpen(e, index)"
@close="e => handleSwipeClose(e, index)"
@content-click="() => closeOther(index)"
:btn-width="80"
:disabled="false"
>
<view class="message-item">
<view class="message-header">
<view class="user-info">
<image class="avatar" :src="item.avatar || defaultAvatar"></image>
<text class="username">{{item.username}}</text>
</view>
<view class="message-time">{{item.time}}</view>
</view>
<view class="message-content">
<view class="question">
<view class="question-icon"></view>
<view class="question-text">{{item.question}}</view>
</view>
<view class="reply-button" @click.stop="handleReply(item)" v-if="!item.reply">
<u-icon name="chat" color="#4a6cf7" size="16"></u-icon>
<text>回复</text>
</view>
<view class="reply-content" v-if="item.reply">
<view class="reply-header">
<view class="reply-icon"></view>
<view class="reply-info">
<text>{{item.replyUser}}</text>
</view>
</view>
<view class="reply-text" :class="{'expanded': item.expanded}">{{item.reply}}</view>
<view class="expand-button" v-if="item.reply.length > 100" @click.stop="toggleExpand(item)">
{{ item.expanded ? '收起' : '展开' }} <u-icon :name="item.expanded ? 'arrow-up' : 'arrow-down'" size="12"></u-icon>
</view>
</view>
<view class="reply-button" @click.stop="handleReply(item)" v-if="item.reply">
<u-icon name="chat" color="#4a6cf7" size="16"></u-icon>
<text>回复</text>
</view>
</view>
</view>
</u-swipe-action>
</view>
<!-- 无数据提示 -->
<view class="empty-tip" v-if="currentMessages.length === 0">
暂无留言数据
</view>
</scroll-view>
</div>
<!-- 使用TabBar组件 -->
<TabBar :currentPath="'/pages/notes/index'" @change="handleTabChange" />
<!-- 开发中提示弹窗 -->
<u-modal
v-model="showDevModal"
:show-cancel-button="false"
title="提示"
content="该功能正在开发中,敬请期待!"
@confirm="showDevModal = false"
></u-modal>
</div>
</template>
<script>
import TabBar from '@/components/TabBar.vue';
export default {
name: 'NotesPage',
components: {
TabBar
},
data() {
return {
activeTab: 'unread',
showDevModal: false,
defaultAvatar: '/static/avatar/default-avatar.png',
unreadMessages: [
{
id: 1,
avatar: '',
username: '浙江理工生13024',
time: '2023/6/26 15:45:12',
question: '学校在录取时有没有一些专业会有特殊要求?',
reply: '',
expanded: false,
show: false
},
{
id: 2,
avatar: '',
username: '理工13024',
time: '2023/6/26 15:45:12',
question: '在录取时有没有一些专业会有特殊要求?',
reply: '',
expanded: false,
show: false
}
],
repliedMessages: [
{
id: 3,
avatar: '',
username: '浙江理工生13024',
time: '2023/6/26 15:45:12',
question: '学校在录取时有没有一些专业会有特殊要求?',
reply: '学生与体健康状况必须符合教育部、卫生部、中国残疾人联合会印发的《普通高等学校招生体检工作指导意见》和人力资源社会保障部、原卫生部、原教育部、原公安部、原国家人口计划生育委员会制定的有关规定。原卫生部、原教育部、原公安部、原国家人口计划生育委员会制定的有关规定。',
replyUser: '系统回复',
expanded: false,
show: false
}
],
swipeOptions: [
{
text: '删除',
style: {
backgroundColor: '#fa3534',
color: '#ffffff',
fontSize: '15px'
}
}
]
};
},
computed: {
currentMessages() {
return this.activeTab === 'unread' ? this.unreadMessages : this.repliedMessages;
}
},
methods: {
switchTab(tab) {
this.activeTab = tab;
//
this.closeAllSwipe();
},
handleTabChange(path, index) {
console.log('切换到标签页:', path, index);
},
handleReply(item) {
this.showDevModal = true;
},
toggleExpand(item) {
item.expanded = !item.expanded;
},
handleSwipeClick(e) {
const {index} = e; //
const btnIndex = e.index; //
if (btnIndex === 0) { //
//
if (this.activeTab === 'unread') {
this.unreadMessages.splice(index, 1);
} else {
this.repliedMessages.splice(index, 1);
}
}
},
handleSwipeOpen(e, index) {
//
this.closeOther(index);
},
handleSwipeClose(e, index) {
//
if (this.activeTab === 'unread') {
this.unreadMessages[index].show = false;
} else {
this.repliedMessages[index].show = false;
}
},
closeOther(index) {
//
if (this.activeTab === 'unread') {
this.unreadMessages.forEach((item, idx) => {
if (idx !== index) {
item.show = false;
}
});
} else {
this.repliedMessages.forEach((item, idx) => {
if (idx !== index) {
item.show = false;
}
});
}
},
closeAllSwipe() {
//
this.unreadMessages.forEach(item => {
item.show = false;
});
this.repliedMessages.forEach(item => {
item.show = false;
});
}
}
}
</script>
<style scoped>
.notes-page {
height: 100vh;
background-color: #f5f6fa;
display: flex;
flex-direction: column;
position: relative;
}
.header {
background-color: #fff;
padding-top: 10px;
}
.title {
font-size: 18px;
font-weight: 500;
text-align: center;
padding: 10px 0;
}
/* 顶部切换标签样式 */
.tab-header {
display: flex;
height: 40px;
border-bottom: 1px solid #f0f0f0;
}
.tab-item {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
color: #333;
position: relative;
}
.tab-item.active {
color: #4A6CF7;
font-weight: 500;
}
.tab-item.active::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 2px;
background-color: #4A6CF7;
}
.content-wrapper {
flex: 1;
display: flex;
flex-direction: column;
padding-bottom: 60px; /* 为底部导航栏预留空间 */
overflow-y: auto; /* 允许内容滚动 */
}
/* 留言列表样式 */
.message-list {
flex: 1;
padding: 10px;
}
.message-item {
background-color: #fff;
padding: 15px;
margin-bottom: 10px;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
.message-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.user-info {
display: flex;
align-items: center;
}
.avatar {
width: 24px;
height: 24px;
border-radius: 50%;
margin-right: 8px;
background-color: #eee;
}
.username {
font-size: 12px;
color: #666;
}
.message-time {
font-size: 12px;
color: #999;
}
.message-content {
position: relative;
}
.question {
display: flex;
margin-bottom: 10px;
}
.question-icon {
width: 20px;
height: 20px;
border-radius: 50%;
background-color: #4a6cf7;
color: #fff;
font-size: 12px;
display: flex;
justify-content: center;
align-items: center;
margin-right: 8px;
flex-shrink: 0;
}
.question-text {
font-size: 14px;
line-height: 1.5;
flex: 1;
}
.reply-button {
height: 32px;
background-color: #f0f2fd;
color: #4a6cf7;
border-radius: 4px;
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
margin-top: 10px;
cursor: pointer;
}
.reply-button text {
margin-left: 5px;
}
.reply-content {
background-color: #f8f8f8;
border-radius: 4px;
padding: 10px;
margin-top: 10px;
}
.reply-header {
display: flex;
align-items: center;
margin-bottom: 5px;
}
.reply-icon {
width: 20px;
height: 20px;
border-radius: 50%;
background-color: #ff9500;
color: #fff;
font-size: 12px;
display: flex;
justify-content: center;
align-items: center;
margin-right: 8px;
}
.reply-info {
font-size: 12px;
color: #666;
}
.reply-text {
font-size: 14px;
line-height: 1.5;
color: #333;
margin-left: 28px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
.reply-text.expanded {
-webkit-line-clamp: unset;
}
.expand-button {
text-align: right;
font-size: 12px;
color: #4a6cf7;
margin-top: 5px;
cursor: pointer;
}
.empty-tip {
text-align: center;
padding: 20px;
color: #999;
font-size: 14px;
}
</style>

View File

@ -0,0 +1 @@
<!-- 图标文件内容,这是二进制文件,实际使用时需要上传真实图片 -->

BIN
static/notes/banner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 KiB

BIN
static/notes/bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
static/notes/delete.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 749 B

BIN
static/notes/major-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
static/notes/menu1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
static/notes/menu2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
static/notes/menu3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
static/notes/形状 790.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
static/notes/组 128(1).png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
static/notes/组 128(2).png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
static/notes/组 128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

BIN
static/notes/组 128@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 743 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 786 B