智慧教务系统
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

870 lines
21 KiB

<!--服务详情页面-->
<template>
<view class="container">
<view class="main-content">
<!-- 加载状态 -->
<view v-if="loading" class="loading-container">
<uni-load-more status="loading" content-text="加载中..."></uni-load-more>
</view>
<!-- 服务记录列表 -->
<view v-else class="service-cards">
<view class="service-card" v-for="(service, index) in serviceList" :key="index" @click="viewServiceDetail(service)">
<!-- 服务预览图 -->
<view class="service-preview" v-if="service.preview_image_url">
<image :src="getImageUrl(service.preview_image_url)" class="preview-image" mode="aspectFill"></image>
</view>
<view class="card-content">
<!-- 服务名称和状态 -->
<view class="card-header">
<view class="service-name">{{ service.service_name }}</view>
<view class="service-status" :class="service.status === 1 ? 'status-active' : (service.status === 0 ? 'status-pending' : 'status-inactive')">
{{ service.status === 1 ? '已完成' : (service.status === 0 ? '待处理' : '未知状态') }}
</view>
</view>
<!-- 服务信息 -->
<view class="service-info">
<view class="info-item" v-if="service.service_type">
<text class="label">服务类型:</text>
<text class="value">{{ service.service_type }}</text>
</view>
<view class="info-item" v-if="service.description">
<text class="label">服务描述:</text>
<text class="value">{{ service.description }}</text>
</view>
<view class="info-item" v-if="service.service_remark || service.status !== 1">
<text class="label">服务结果:</text>
<view class="value-content" v-if="service.service_remark">
<rich-text :nodes="formatRichText(service.service_remark)"></rich-text>
</view>
<text class="value placeholder" v-else-if="service.status !== 1">点击编辑服务结果</text>
</view>
<view class="info-item" v-if="service.feedback">
<text class="label">家长反馈:</text>
<text class="value">{{ service.feedback }}</text>
</view>
<view class="info-item" v-if="service.score">
<text class="label">家长评分:</text>
<text class="value score">{{ service.score }}分</text>
</view>
<view class="info-item">
<text class="label">创建时间:</text>
<text class="value">{{ formatDate(service.created_at) }}</text>
</view>
</view>
<!-- 操作按钮 -->
<view class="card-actions" v-if="service.status !== 1">
<button class="edit-btn" @click.stop="editServiceRemark(service)">
<text class="iconfont icon-edit"></text>
编辑服务结果
</button>
</view>
</view>
<!-- 箭头图标 -->
<view class="card-arrow">
<text class="iconfont icon-arrow-right"></text>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-if="!loading && serviceList.length === 0">
<view class="empty-icon">📝</view>
<view class="empty-text">暂无服务记录</view>
</view>
<!-- 底部加载更多 -->
<view v-if="hasMore && !loading && serviceList.length > 0" class="load-more">
<uni-load-more :status="loadMoreStatus" @clickLoadMore="loadMore"></uni-load-more>
</view>
</view>
<!-- 编辑服务结果弹窗 -->
<view v-if="showEditModal" class="edit-modal" @click="closeEditModal">
<view class="modal-content" @click.stop>
<view class="modal-header">
<text class="modal-title">编辑服务结果</text>
<view class="modal-close" @click="closeEditModal">
<text class="iconfont icon-close"></text>
</view>
</view>
<view class="modal-body">
<view class="form-item">
<text class="form-label">服务结果:</text>
<!-- 富文本工具栏 -->
<view class="editor-toolbar">
<view class="toolbar-group">
<view class="tool-btn" :class="{ active: bold }" @click="toggleBold">
<text class="tool-text">B</text>
</view>
<view class="tool-btn" :class="{ active: italic }" @click="toggleItalic">
<text class="tool-text">I</text>
</view>
<view class="tool-btn" @click="insertBulletList">
<text class="tool-text">•</text>
</view>
<view class="tool-btn" @click="insertNumberList">
<text class="tool-text">1.</text>
</view>
</view>
</view>
<!-- 文本输入区域 -->
<textarea
class="form-textarea"
v-model="editForm.service_remark"
placeholder="请输入服务结果内容,支持简单的格式化文本"
maxlength="1000"
:show-confirm-bar="false"
@focus="onTextareaFocus"
@blur="onTextareaBlur">
</textarea>
<!-- 字数统计 -->
<view class="char-count">{{ editForm.service_remark.length }}/1000</view>
<!-- 提示信息 -->
<view class="editor-tips">
<text class="tip-text">支持基础格式:粗体、斜体、列表等</text>
</view>
</view>
</view>
<view class="modal-footer">
<button class="modal-btn cancel" @click="closeEditModal">取消</button>
<button class="modal-btn confirm" @click="saveServiceRemark" :disabled="saving">
{{ saving ? '保存中...' : '保存' }}
</button>
</view>
</view>
</view>
</view>
</template>
<script>
import apiRoute from '@/common/axios.js';
export default {
data() {
return {
loading: true,
serviceList: [],
currentPage: 1,
pageSize: 10,
hasMore: true,
loadMoreStatus: 'more',
showEditModal: false,
saving: false,
editForm: {
id: 0,
service_remark: ''
},
// 富文本编辑状态
bold: false,
italic: false,
textareaFocused: false
}
},
onLoad() {
this.init();
},
onPullDownRefresh() {
this.refreshServiceList();
},
onReachBottom() {
if (this.hasMore && !this.loading) {
this.loadMore();
}
},
methods: {
async init() {
this.getServiceList();
},
// 获取服务记录列表
async getServiceList(refresh = false) {
if (refresh) {
this.currentPage = 1;
this.hasMore = true;
this.serviceList = [];
}
this.loading = true;
this.loadMoreStatus = 'loading';
try {
const response = await apiRoute.get('/personnel/myServiceLogs', {
page: this.currentPage,
limit: this.pageSize,
demo: 1 // 添加演示数据标识
});
if (response.code === 1) {
const newServices = response.data.data || [];
if (refresh) {
this.serviceList = newServices;
} else {
this.serviceList = [...this.serviceList, ...newServices];
}
// 检查是否还有更多数据
this.hasMore = newServices.length === this.pageSize;
this.loadMoreStatus = this.hasMore ? 'more' : 'noMore';
} else {
uni.showToast({
title: response.data.msg || '加载失败',
icon: 'none'
});
}
} catch (error) {
console.error('获取服务记录失败:', error);
uni.showToast({
title: '网络错误,请稍后重试',
icon: 'none'
});
} finally {
this.loading = false;
if (refresh) {
uni.stopPullDownRefresh();
}
}
},
// 刷新服务列表
refreshServiceList() {
this.getServiceList(true);
},
// 加载更多
loadMore() {
if (this.hasMore && !this.loading) {
this.currentPage++;
this.getServiceList();
}
},
// 查看服务详情
async viewServiceDetail(service) {
try {
const response = await apiRoute.get('/personnel/serviceLogDetail', {
id: service.id
});
if (response.code === 1) {
const detail = response.data;
this.showServiceDetailModal(detail);
} else {
uni.showToast({
title: response.data.msg || '获取详情失败',
icon: 'none'
});
}
} catch (error) {
console.error('获取服务详情失败:', error);
uni.showToast({
title: '网络错误,请稍后重试',
icon: 'none'
});
}
},
// 显示服务详情弹窗
showServiceDetailModal(detail) {
let content = `服务名称:${detail.service_name || '-'}\n`;
content += `服务类型:${detail.service_type || '-'}\n`;
content += `状态:${detail.status === 1 ? '已完成' : (detail.status === 0 ? '待处理' : '未知状态')}\n`;
if (detail.description) {
content += `描述:${detail.description}\n`;
}
if (detail.service_remark) {
content += `服务结果:${detail.service_remark}\n`;
}
if (detail.feedback) {
content += `家长反馈:${detail.feedback}\n`;
}
if (detail.score) {
content += `家长评分:${detail.score}`;
}
uni.showModal({
title: '服务详情',
content: content,
showCancel: false
});
},
// 获取图片URL
getImageUrl(url) {
if (!url) return '';
if (url.startsWith('http')) {
return url;
}
return this.$baseUrl + '/' + url;
},
// 格式化日期
formatDate(dateString) {
if (!dateString) return '-';
const date = new Date(dateString);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
},
// 去除HTML标签
stripHtml(html) {
if (!html) return '';
return html.replace(/<[^>]*>/g, '').trim();
},
// 格式化富文本内容
formatRichText(text) {
if (!text) return '';
// 简单的markdown转换
let formatted = text
// 粗体
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
// 斜体
.replace(/\*(.*?)\*/g, '<em>$1</em>')
// 无序列表
.replace(/^\u2022\s(.+)$/gm, '<li>$1</li>')
// 有序列表
.replace(/^\d+\.\s(.+)$/gm, '<li>$1</li>')
// 换行
.replace(/\n/g, '<br/>');
// 如果有列表项,包装在ul标签中
if (formatted.includes('<li>')) {
formatted = formatted.replace(/(<li>.*?<\/li>)/g, '<ul>$1</ul>');
}
return formatted;
},
// 编辑服务结果
editServiceRemark(service) {
// 检查是否可以编辑
if (service.status === 1) {
uni.showToast({
title: '服务已完成,无法修改',
icon: 'none'
});
return;
}
this.editForm.id = service.id;
this.editForm.service_remark = this.stripHtml(service.service_remark || '');
this.showEditModal = true;
},
// 关闭编辑弹窗
closeEditModal() {
this.showEditModal = false;
this.editForm = {
id: 0,
service_remark: ''
};
},
// 保存服务结果
async saveServiceRemark() {
if (!this.editForm.service_remark.trim()) {
uni.showToast({
title: '请输入服务结果内容',
icon: 'none'
});
return;
}
this.saving = true;
try {
const response = await apiRoute.post('/personnel/updateServiceRemark', {
id: this.editForm.id,
service_remark: this.editForm.service_remark
});
if (response.data.code === 1) {
uni.showToast({
title: '保存成功',
icon: 'success'
});
// 更新列表中的数据
const index = this.serviceList.findIndex(item => item.id === this.editForm.id);
if (index !== -1) {
this.serviceList[index].service_remark = this.editForm.service_remark;
}
this.closeEditModal();
} else {
uni.showToast({
title: response.data.msg || '保存失败',
icon: 'none'
});
}
} catch (error) {
console.error('保存服务结果失败:', error);
uni.showToast({
title: '网络错误,请稍后重试',
icon: 'none'
});
} finally {
this.saving = false;
}
},
// 富文本编辑功能
onTextareaFocus() {
this.textareaFocused = true;
},
onTextareaBlur() {
this.textareaFocused = false;
},
toggleBold() {
this.bold = !this.bold;
this.insertFormatText('**', '**');
},
toggleItalic() {
this.italic = !this.italic;
this.insertFormatText('*', '*');
},
insertBulletList() {
this.insertFormatText('\n• ', '');
},
insertNumberList() {
this.insertFormatText('\n1. ', '');
},
insertFormatText(before, after) {
const textarea = this.editForm.service_remark;
const newText = textarea + before + '请输入内容' + after;
this.editForm.service_remark = newText;
}
}
}
</script>
<style lang="scss" scoped>
.container {
background: #1a1a1a;
min-height: 100vh;
}
.main-content {
padding: 20rpx;
}
.service-cards {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.service-card {
background: #2a2a2a;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.3);
border: 1rpx solid #444;
position: relative;
display: flex;
align-items: center;
padding: 24rpx;
}
.service-preview {
width: 120rpx;
height: 120rpx;
border-radius: 12rpx;
overflow: hidden;
margin-right: 24rpx;
flex-shrink: 0;
.preview-image {
width: 100%;
height: 100%;
}
}
.card-content {
flex: 1;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
.service-name {
font-size: 32rpx;
font-weight: 600;
color: #fff;
flex: 1;
margin-right: 16rpx;
}
.service-status {
padding: 8rpx 16rpx;
border-radius: 20rpx;
font-size: 24rpx;
font-weight: 500;
&.status-active {
background: rgba(41, 211, 180, 0.2);
color: #29d3b4;
border: 1rpx solid #29d3b4;
}
&.status-pending {
background: rgba(255, 193, 7, 0.2);
color: #ffc107;
border: 1rpx solid #ffc107;
}
&.status-inactive {
background: rgba(220, 53, 69, 0.2);
color: #dc3545;
border: 1rpx solid #dc3545;
}
}
}
.service-info {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.info-item {
display: flex;
align-items: flex-start;
.label {
color: #999;
font-size: 26rpx;
min-width: 140rpx;
flex-shrink: 0;
}
.value {
color: #ccc;
font-size: 26rpx;
flex: 1;
word-break: break-all;
&.score {
color: #29d3b4;
font-weight: 600;
}
&.placeholder {
color: #666;
font-style: italic;
}
}
.value-content {
flex: 1;
color: #ccc;
font-size: 26rpx;
line-height: 1.6;
/* 富文本样式 */
:deep(strong) {
font-weight: 600;
color: #fff;
}
:deep(em) {
font-style: italic;
color: #29d3b4;
}
:deep(ul) {
margin: 8rpx 0;
padding-left: 24rpx;
}
:deep(li) {
margin: 4rpx 0;
list-style: disc;
}
}
}
.card-arrow {
margin-left: 16rpx;
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
.iconfont {
font-size: 24rpx;
color: #666;
}
}
.card-actions {
margin-top: 20rpx;
padding-top: 20rpx;
border-top: 1rpx solid #444;
.edit-btn {
background: #29d3b4;
color: #fff;
border: none;
border-radius: 8rpx;
padding: 12rpx 24rpx;
font-size: 26rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
&:active {
background: #22b39a;
}
.iconfont {
font-size: 24rpx;
}
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 40rpx;
.empty-icon {
font-size: 120rpx;
margin-bottom: 24rpx;
opacity: 0.3;
}
.empty-text {
color: #666;
font-size: 28rpx;
}
}
.loading-container {
padding: 120rpx 40rpx;
text-align: center;
}
.load-more {
padding: 40rpx 0;
}
/* 动画效果 */
.service-card {
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(20rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 编辑弹窗样式 */
.edit-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
.modal-content {
background-color: #2a2a2a;
border-radius: 20rpx;
width: 90%;
max-height: 80%;
overflow: hidden;
border: 1rpx solid #444;
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx;
border-bottom: 1rpx solid #444;
.modal-title {
font-size: 32rpx;
font-weight: 600;
color: #fff;
}
.modal-close {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
.iconfont {
font-size: 32rpx;
color: #999;
}
}
}
.modal-body {
padding: 32rpx;
.form-item {
.form-label {
font-size: 28rpx;
color: #ccc;
margin-bottom: 16rpx;
display: block;
}
.editor-toolbar {
background-color: #1a1a1a;
border: 1rpx solid #444;
border-bottom: none;
border-radius: 12rpx 12rpx 0 0;
padding: 16rpx;
.toolbar-group {
display: flex;
gap: 16rpx;
.tool-btn {
width: 48rpx;
height: 48rpx;
background-color: #444;
border: 1rpx solid #666;
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
&.active {
background-color: #29d3b4;
border-color: #29d3b4;
}
.tool-text {
font-size: 24rpx;
color: #fff;
font-weight: 600;
}
}
}
}
.form-textarea {
width: 100%;
min-height: 300rpx;
background-color: #1a1a1a;
border: 1rpx solid #444;
border-radius: 0 0 12rpx 12rpx;
border-top: none;
padding: 20rpx;
font-size: 28rpx;
color: #fff;
line-height: 1.6;
box-sizing: border-box;
&::placeholder {
color: #666;
}
}
.char-count {
text-align: right;
margin-top: 8rpx;
font-size: 24rpx;
color: #666;
}
.editor-tips {
margin-top: 12rpx;
.tip-text {
font-size: 22rpx;
color: #666;
line-height: 1.4;
}
}
}
}
.modal-footer {
display: flex;
gap: 24rpx;
padding: 32rpx;
border-top: 1rpx solid #444;
.modal-btn {
flex: 1;
height: 72rpx;
border-radius: 12rpx;
font-size: 28rpx;
font-weight: 600;
border: none;
display: flex;
align-items: center;
justify-content: center;
&.cancel {
background-color: #444;
color: #ccc;
&:active {
background-color: #555;
}
}
&.confirm {
background-color: #29d3b4;
color: #fff;
&:active:not(:disabled) {
background-color: #22b39a;
}
&:disabled {
background-color: #666;
color: #999;
}
}
}
}
}
}
</style>