资源调整

This commit is contained in:
hebo 2025-10-31 12:04:23 +08:00
parent 33643f78ae
commit f525a6ae77
14 changed files with 2463 additions and 201 deletions

View File

@ -204,6 +204,48 @@ export const zymlFindTreeByZylxApi = async (params: any) => {
return await get("/api/zyml/qryTreeByZylx", params);
};
// ==================== 资源包Zy相关API ====================
// 获取资源包分页列表
export const zyFindPageApi = async (params: any) => {
return await get("/api/zy/findPage", params);
};
// 保存资源包(新增/编辑)
export const zySaveApi = async (params: any) => {
return await post("/api/zy/save", params);
};
// 收藏/取消收藏资源包
export const zyCollectApi = async (params: { zyId: string }) => {
return await post(`/api/zy/collect?zyId=${params.zyId}`);
};
// 点赞/取消点赞资源包
export const zyLikeApi = async (params: { zyId: string }) => {
return await post(`/api/zy/like?zyId=${params.zyId}`);
};
// 评分资源包
export const zyScoreApi = async (params: { zyId: string; score: number; comment?: string }) => {
const queryParams = new URLSearchParams({
zyId: params.zyId,
score: params.score.toString(),
...(params.comment ? { comment: params.comment } : {})
});
return await post(`/api/zy/score?${queryParams.toString()}`);
};
// 根据ID查询资源包详情包含资源明细和用户状态
export const zyGetDetailApi = async (params: { id: string }) => {
return await get("/api/zy/getDetail", params);
};
// 删除资源包
export const zyLogicDeleteApi = async (params: { ids: string }) => {
return await post("/api/zy/logicDelete", params);
};
// ==================== 资源明细Zymx相关API ====================
// 获取资源明细分页
export const zymxFindPageApi = async (params: any) => {
return await get("/api/zymx/findPage", params);

View File

@ -381,6 +381,27 @@
"enablePullDownRefresh": false
}
},
{
"path": "pages/view/routine/JiaoXueZiYuan/zy-detail",
"style": {
"navigationBarTitleText": "资源包详情",
"enablePullDownRefresh": false
}
},
{
"path": "pages/view/routine/JiaoXueZiYuan/zy-list",
"style": {
"navigationBarTitleText": "教学资源包",
"enablePullDownRefresh": false
}
},
{
"path": "pages/view/routine/JiaoXueZiYuan/zy-add",
"style": {
"navigationBarTitleText": "新增资源包",
"enablePullDownRefresh": false
}
},
{
"path": "pages/view/routine/HuoDongZiYuan/index",
"style": {

View File

@ -442,7 +442,7 @@ const sections = reactive<Section[]>([
{
id: "r5",
icon: "stxc",
text: "食堂巡查",
text: "食堂日志",
show: true,
permissionKey: "routine-stxc", //
path: "/pages/view/routine/ShiTangXunCha/index",

View File

@ -133,8 +133,10 @@ const goTo = function () {
}
const params = {
resourType: leafNode.key, // key
category: curCategory.value.key,
zymlId: leafNode.key, // ID
zyCategory: curCategory.value.key, //
resourType: leafNode.key, //
category: curCategory.value.key, //
};
console.log('选择的参数:', {
@ -148,8 +150,10 @@ const goTo = function () {
});
setData(params);
//
uni.navigateTo({
url: `/pages/view/routine/JiaoXueZiYuan/indexList`
url: `/pages/view/routine/JiaoXueZiYuan/zy-list`
});
}

View File

@ -0,0 +1,703 @@
<template>
<BasicLayout :show-nav-bar="true" :nav-bar-props="{ title: modalTitle }">
<view class="add-page">
<!-- 基本信息 -->
<view class="form-section">
<view class="section-title">基本信息</view>
<!-- 资源包名称 -->
<view class="form-item">
<text class="form-label"><text class="required">*</text> 资源包名称</text>
<input
v-model="formData.zyName"
placeholder="请输入资源包名称"
class="form-input"
/>
</view>
<!-- 资源目录 -->
<view class="form-item">
<text class="form-label"><text class="required">*</text> 资源目录</text>
<view class="picker-box" @click="showResourceTree">
<text :class="{ placeholder: !formData.zymlId }">
{{ selectedTreeText || '请选择资源目录' }}
</text>
<uni-icons type="right" size="16" color="#999"></uni-icons>
</view>
</view>
<!-- 资源类别 -->
<view class="form-item">
<text class="form-label"><text class="required">*</text> 资源类别</text>
<picker
mode="selector"
:range="categoryList"
range-key="label"
@change="handleCategoryChange"
>
<view class="picker-box">
<text :class="{ placeholder: !formData.zyCategory }">
{{ getCategoryText() || '请选择资源类别' }}
</text>
<uni-icons type="right" size="16" color="#999"></uni-icons>
</view>
</picker>
</view>
<!-- 资源描述 -->
<view class="form-item">
<text class="form-label">资源描述</text>
<textarea
v-model="formData.zyDesc"
placeholder="请输入资源描述"
class="form-textarea"
maxlength="500"
></textarea>
</view>
</view>
<!-- 上传资源 -->
<view class="form-section">
<view class="section-title">
<text>上传资源</text>
<text class="required">*</text>
</view>
<!-- 上传按钮 -->
<view class="upload-box" @click="chooseFile">
<uni-icons type="cloud-upload" size="40" color="#1890ff"></uni-icons>
<text class="upload-text">点击选择文件上传</text>
<text class="upload-hint">支持 .doc.pdf.ppt.xls 等格式</text>
</view>
<!-- 已上传文件列表 -->
<view v-if="uploadedFiles.length > 0" class="files-list">
<view class="list-header">已上传文件 ({{ uploadedFiles.length }})</view>
<view
v-for="(file, index) in uploadedFiles"
:key="index"
class="file-item"
>
<view class="file-info">
<text class="file-index">{{ index + 1 }}</text>
<view class="file-detail">
<text class="file-name">{{ file.fileName }}</text>
<text class="file-meta">{{ file.fileType }} {{ formatFileSize(file.fileSize) }}</text>
</view>
</view>
<!-- 资源类型选择 -->
<picker
mode="selector"
:range="resourceTypeOptions"
range-key="label"
:value="getResourceTypeIndex(file.category)"
@change="(e: any) => handleFileTypeChange(e, index)"
>
<view class="type-picker">
<text :class="{ placeholder: !file.category }">
{{ getResourceTypeText(file.category) || '选择类型' }}
</text>
<uni-icons type="right" size="14" color="#999"></uni-icons>
</view>
</picker>
<!-- 删除按钮 -->
<view class="file-remove" @click="removeFile(index)">
<uni-icons type="trash" size="18" color="#ff4d4f"></uni-icons>
</view>
</view>
</view>
</view>
</view>
<!-- 底部操作栏 -->
<template #bottom>
<view class="bottom-actions">
<u-button text="取消" @click="handleCancel" class="action-btn"></u-button>
<u-button text="提交" type="primary" @click="handleSubmit" :loading="submitLoading" class="action-btn"></u-button>
</view>
</template>
</BasicLayout>
<!-- 资源目录树选择 -->
<BasicTree
ref="treeRef"
:range="treeData"
idKey="key"
rangeKey="title"
title="选择资源目录"
:multiple="false"
:selectParent="true"
@confirm="onTreeConfirm"
@cancel="onTreeCancel"
/>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import BasicTree from '@/components/BasicTree/Tree.vue';
import {
zySaveApi,
zymlFindTreeApi,
dicFindByPidApi
} from "@/api/base/server";
import { attachmentUpload } from "@/api/system/upload";
const modalTitle = ref('新增资源包');
const formData = ref<any>({
zyName: '',
zymlId: '',
zyCategory: '',
zyDesc: ''
});
const uploadedFiles = ref<any[]>([]);
const submitLoading = ref(false);
//
const treeData = ref<any[]>([]);
const treeRef = ref();
const selectedTreeText = ref('');
//
const categoryList = ref<any[]>([]);
// -
const resourceTypeOptions = ref<any[]>([]);
//
const getCategoryText = () => {
const item = categoryList.value.find(c => c.value === formData.value.zyCategory);
return item?.label || '';
};
//
const getResourceTypeText = (typeValue: string) => {
const item = resourceTypeOptions.value.find(t => t.value === typeValue);
return item?.label || '';
};
//
const getResourceTypeIndex = (typeValue: string) => {
return resourceTypeOptions.value.findIndex(t => t.value === typeValue);
};
//
const handleCategoryChange = (e: any) => {
formData.value.zyCategory = categoryList.value[e.detail.value]?.value;
};
//
const handleFileTypeChange = (e: any, index: number) => {
uploadedFiles.value[index].category = resourceTypeOptions.value[e.detail.value]?.value;
};
//
const showResourceTree = () => {
if (treeRef.value) {
treeRef.value._show();
}
};
//
const onTreeConfirm = (selectedItems: any[]) => {
if (selectedItems.length > 0) {
const selectedItem = selectedItems[0]; //
formData.value.zymlId = selectedItem.key;
selectedTreeText.value = selectedItem.title;
}
};
//
const onTreeCancel = () => {
//
};
//
const chooseFile = () => {
uni.chooseFile({
count: 10,
type: 'all',
//
success: (res) => {
const files = Array.isArray(res.tempFiles) ? res.tempFiles : [res.tempFiles];
files.forEach((file: any) => {
uploadFile(file);
});
},
fail: (error) => {
console.error('选择文件失败:', error);
uni.showToast({
title: '选择文件失败',
icon: 'none'
});
}
});
};
//
const uploadFile = async (file: any) => {
uni.showLoading({ title: '上传中...' });
try {
// 使 attachmentUpload
const uploadResult: any = await attachmentUpload(file.path as any);
console.log('上传响应:', uploadResult);
if (uploadResult.resultCode === 1 && uploadResult.result && uploadResult.result.length > 0) {
const fileInfo = uploadResult.result[0];
uploadedFiles.value.push({
fileName: file.name,
filePath: fileInfo.filePath,
fileId: fileInfo.id,
fileType: getFileExtension(file.name),
fileSize: file.size,
category: '' //
});
uni.showToast({
title: '上传成功',
icon: 'success'
});
} else if (uploadResult.resultCode === 1 && uploadResult.result && uploadResult.result.length === 0) {
console.error('上传成功但返回数据为空:', uploadResult);
uni.showToast({
title: '上传失败:服务器未返回文件信息',
icon: 'none',
duration: 2500
});
} else {
uni.showToast({
title: uploadResult.message || '上传失败',
icon: 'none'
});
}
} catch (error) {
console.error('上传失败:', error);
uni.showToast({
title: '上传失败',
icon: 'none'
});
} finally {
uni.hideLoading();
}
};
//
const getFileExtension = (fileName: string) => {
const parts = fileName.split('.');
return parts.length > 1 ? parts[parts.length - 1] : '';
};
//
const formatFileSize = (size: number) => {
if (!size) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(size) / Math.log(k));
return (size / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i];
};
//
const removeFile = (index: number) => {
uploadedFiles.value.splice(index, 1);
};
//
const handleCancel = () => {
uni.navigateBack();
};
//
const handleSubmit = async () => {
//
if (!formData.value.zyName) {
uni.showToast({
title: '请输入资源包名称',
icon: 'none'
});
return;
}
if (!formData.value.zymlId) {
uni.showToast({
title: '请选择资源目录',
icon: 'none'
});
return;
}
if (!formData.value.zyCategory) {
uni.showToast({
title: '请选择资源类别',
icon: 'none'
});
return;
}
if (uploadedFiles.value.length === 0) {
uni.showToast({
title: '请至少上传一个资源文件',
icon: 'none'
});
return;
}
//
const unselectedFiles = uploadedFiles.value.filter(file => !file.category);
if (unselectedFiles.length > 0) {
uni.showToast({
title: '请为所有文件选择资源类型',
icon: 'none'
});
return;
}
submitLoading.value = true;
try {
//
const submitData: any = {
zyName: formData.value.zyName,
zymlId: formData.value.zymlId,
zyCategory: formData.value.zyCategory,
zyDesc: formData.value.zyDesc || '',
// sort
zymxList: uploadedFiles.value.map((file, index) => ({
resourName: file.fileName,
resourUrl: file.filePath,
resourId: file.fileId,
resSuf: file.fileType,
category: file.category,
sort: index + 1
}))
};
console.log('提交资源包数据:', submitData);
const res = await zySaveApi(submitData);
if (res.resultCode === 1) {
uni.showToast({
title: '保存成功',
icon: 'success'
});
//
uni.$emit('refreshResourceList');
setTimeout(() => {
uni.navigateBack();
}, 1500);
} else {
uni.showToast({
title: res.message || '保存失败',
icon: 'none'
});
}
} catch (error) {
console.error('提交失败:', error);
uni.showToast({
title: '提交失败',
icon: 'none'
});
} finally {
submitLoading.value = false;
}
};
//
const loadTreeData = async () => {
try {
const res = await zymlFindTreeApi();
if (res && res.result) {
// BasicTree
const convertTreeData = (items: any[]): any[] => {
return items.map((item: any) => ({
key: item.key,
title: item.title,
value: item.value,
children: item.children ? convertTreeData(item.children) : [],
}));
};
treeData.value = convertTreeData(res.result);
}
} catch (error) {
console.error('加载资源目录失败:', error);
}
};
//
const loadCategoryList = async () => {
try {
const pid = '5'; // ID
const res = await dicFindByPidApi({ pid });
if (res && res.result && Array.isArray(res.result)) {
categoryList.value = res.result.map((item: any) => ({
value: item.dictionaryCode || item.dictionaryValue || item.id,
label: item.dictionaryName || item.dictionaryValue || item.name
}));
} else {
categoryList.value = [
{ value: '1', label: '课程包' },
{ value: '2', label: '练习包' },
{ value: '3', label: '试卷包' }
];
}
//
if (categoryList.value.length > 0 && !formData.value.zyCategory) {
formData.value.zyCategory = categoryList.value[0].value;
}
} catch (error) {
console.error('加载资源类别失败:', error);
}
};
//
const loadResourceTypeOptions = async () => {
try {
const pid = '1391443399'; // ID
const res = await dicFindByPidApi({ pid });
if (res && res.result && Array.isArray(res.result)) {
resourceTypeOptions.value = res.result.map((item: any) => ({
value: item.dictionaryCode || item.dictionaryValue || item.id,
label: item.dictionaryName || item.dictionaryValue || item.name
}));
} else {
resourceTypeOptions.value = [
{ value: '1', label: '课件' },
{ value: '2', label: '教案' },
{ value: '3', label: '学案' },
{ value: '4', label: '作业' },
{ value: '5', label: '试卷' }
];
}
} catch (error) {
console.error('加载资源类型失败:', error);
}
};
onLoad(async () => {
await loadTreeData();
await loadCategoryList();
await loadResourceTypeOptions();
});
</script>
<style scoped lang="scss">
.add-page {
min-height: 100vh;
background: #f5f5f5;
padding: 20rpx;
padding-bottom: 140rpx;
}
.form-section {
background: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 25rpx;
padding-bottom: 15rpx;
border-bottom: 1rpx solid #f0f0f0;
.required {
color: #ff4d4f;
margin-left: 5rpx;
}
}
.form-item {
margin-bottom: 30rpx;
&:last-child {
margin-bottom: 0;
}
.form-label {
display: block;
font-size: 28rpx;
color: #333;
margin-bottom: 15rpx;
.required {
color: #ff4d4f;
margin-right: 5rpx;
}
}
.form-input {
width: 100%;
height: 70rpx;
padding: 0 25rpx;
border: 1rpx solid #e0e0e0;
border-radius: 8rpx;
font-size: 28rpx;
box-sizing: border-box;
}
.form-textarea {
width: 100%;
min-height: 150rpx;
padding: 20rpx;
border: 1rpx solid #e0e0e0;
border-radius: 8rpx;
font-size: 28rpx;
box-sizing: border-box;
}
.picker-box {
display: flex;
align-items: center;
justify-content: space-between;
height: 70rpx;
padding: 0 25rpx;
border: 1rpx solid #e0e0e0;
border-radius: 8rpx;
font-size: 28rpx;
.placeholder {
color: #999;
}
}
}
}
.upload-box {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60rpx;
border: 2rpx dashed #d9d9d9;
border-radius: 8rpx;
background: #fafafa;
.upload-text {
margin-top: 20rpx;
font-size: 28rpx;
color: #666;
}
.upload-hint {
margin-top: 10rpx;
font-size: 24rpx;
color: #999;
}
}
.files-list {
margin-top: 30rpx;
border: 1rpx solid #f0f0f0;
border-radius: 8rpx;
overflow: hidden;
.list-header {
padding: 20rpx 25rpx;
background: #fafafa;
font-size: 28rpx;
font-weight: bold;
color: #333;
border-bottom: 1rpx solid #f0f0f0;
}
.file-item {
display: flex;
align-items: center;
padding: 20rpx 25rpx;
border-bottom: 1rpx solid #f0f0f0;
gap: 15rpx;
&:last-child {
border-bottom: none;
}
.file-info {
flex: 1;
display: flex;
align-items: center;
gap: 15rpx;
min-width: 0;
.file-index {
width: 40rpx;
height: 40rpx;
background: #1890ff;
color: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 22rpx;
flex-shrink: 0;
}
.file-detail {
flex: 1;
min-width: 0;
.file-name {
display: block;
font-size: 26rpx;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-bottom: 8rpx;
}
.file-meta {
font-size: 22rpx;
color: #999;
}
}
}
.type-picker {
display: flex;
align-items: center;
gap: 8rpx;
padding: 10rpx 20rpx;
background: #f5f5f5;
border-radius: 6rpx;
font-size: 24rpx;
flex-shrink: 0;
.placeholder {
color: #999;
}
}
.file-remove {
flex-shrink: 0;
padding: 10rpx;
}
}
}
.bottom-actions {
display: flex;
gap: 20rpx;
padding: 20rpx;
background: #fff;
border-top: 1rpx solid #f0f0f0;
.action-btn {
flex: 1;
}
}
</style>

View File

@ -0,0 +1,808 @@
<template>
<BasicLayout :show-nav-bar="true" :nav-bar-props="{ title: '资源包详情' }">
<view class="detail-page">
<view v-if="loading" class="loading-container">
<text class="loading-text">加载中...</text>
</view>
<view v-else-if="resourceDetail" class="detail-content">
<!-- 资源包信息卡片 -->
<view class="info-card">
<view class="package-header">
<text class="package-title">{{ resourceDetail.zyName }}</text>
<view class="package-category">{{ getCategoryName(resourceDetail.zyCategory) }}</view>
</view>
<!-- 评分和操作区域 -->
<view class="rating-action-section">
<!-- 左侧评分展示 -->
<view class="rating-display">
<text class="rating-score">{{ (resourceDetail.scoreAvg || 0).toFixed(1) }}</text>
<view class="rating-stars-wrapper">
<text class="rating-stars">{{ getStarDisplay(resourceDetail.scoreAvg) }}</text>
</view>
<text class="rating-label">当前评分</text>
</view>
<!-- 右侧操作按钮 -->
<view class="action-buttons">
<view class="action-item" @click="handleRating">
<uni-icons type="star" size="24" color="#faad14"></uni-icons>
<text class="action-label">评分</text>
</view>
<view class="action-item" @click="handleCollect">
<uni-icons
:type="resourceDetail.isCollect === '1' ? 'heart-filled' : 'heart'"
size="24"
:color="resourceDetail.isCollect === '1' ? '#ff4d4f' : '#999'"
></uni-icons>
<text class="action-label">{{ resourceDetail.isCollect === '1' ? '已收藏' : '收藏' }}</text>
<text class="action-count">{{ formatNumber(resourceDetail.collNum || 0) }}</text>
</view>
<view class="action-item" @click="handleLike">
<view class="like-icon">
<text class="like-emoji">{{ resourceDetail.isLike === '1' ? '👍' : '👍🏻' }}</text>
</view>
<text class="action-label">{{ resourceDetail.isLike === '1' ? '已点赞' : '点赞' }}</text>
<text class="action-count">{{ formatNumber(resourceDetail.likeNum || 0) }}</text>
</view>
</view>
</view>
<!-- 描述 -->
<view class="description-section">
<text class="section-title">资源描述</text>
<text class="description-text">{{ resourceDetail.zyDesc || '暂无描述' }}</text>
</view>
</view>
<!-- 包含的资源列表 -->
<view class="resources-card">
<view class="card-title">
包含的资源 ({{ resourceDetail.zymxList?.length || 0 }})
</view>
<view v-if="resourceDetail.zymxList && resourceDetail.zymxList.length > 0">
<view
v-for="(item, index) in resourceDetail.zymxList"
:key="item.id"
class="resource-item"
>
<view class="item-index">{{ index + 1 }}</view>
<view class="item-info">
<text class="item-name">{{ item.resourName }}</text>
<view class="item-meta">
<text class="meta-tag">{{ item.resSuf }}</text>
<text class="meta-stats">浏览 {{ item.lookNum || 0 }}</text>
<text class="meta-stats">下载 {{ item.downNum || 0 }}</text>
</view>
</view>
<view class="item-actions">
<u-button
v-if="canPreview(item.resSuf)"
size="mini"
type="primary"
@click="handlePreview(item)"
>
预览
</u-button>
<u-button
size="mini"
type="success"
@click="handleDownload(item)"
>
下载
</u-button>
</view>
</view>
</view>
<view v-else class="empty-state">
<text class="empty-text">暂无资源</text>
</view>
</view>
</view>
<view v-else class="error-container">
<text class="error-text">加载失败</text>
</view>
</view>
<!-- 底部返回按钮 -->
<template #bottom>
<view class="bottom-actions">
<u-button
type="primary"
@click="handleBack"
class="back-button"
>
返回
</u-button>
</view>
</template>
</BasicLayout>
<!-- 评分弹窗使用原生 modal-->
<view v-if="showRatingModal" class="rating-modal-mask" @click="closeRatingModal">
<view class="rating-modal" @click.stop>
<view class="modal-title">资源评分</view>
<view class="modal-content">
<text class="resource-name">{{ resourceDetail?.zyName }}</text>
<view class="rating-input">
<view class="star-picker">
<view
v-for="star in 10"
:key="star"
class="star-item"
@click="selectRating(star / 2)"
>
<text class="star-icon">{{ getStarIcon(star / 2, ratingValue) }}</text>
</view>
</view>
<text class="rating-value">{{ ratingValue.toFixed(1) }}</text>
</view>
<textarea
v-model="ratingComment"
placeholder="请输入评价内容(选填)"
maxlength="200"
class="comment-textarea"
></textarea>
<text class="char-count">{{ ratingComment.length }}/200</text>
</view>
<view class="modal-actions">
<button class="modal-btn cancel-btn" @click="closeRatingModal">取消</button>
<button class="modal-btn confirm-btn" @click="submitRating">确认</button>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import { imagUrl } from "@/utils";
import {
zyGetDetailApi,
zyCollectApi,
zyLikeApi,
zyScoreApi,
dicFindByPidApi
} from "@/api/base/server";
import {
isVideo,
isImage,
canPreview,
previewFile,
previewVideo,
previewImage,
downloadFile
} from "@/utils/filePreview";
const resourceId = ref<string>('');
const resourceDetail = ref<any>(null);
const loading = ref(false);
const categoryOptions = ref<any[]>([]);
const showRatingModal = ref(false);
const ratingValue = ref(0);
const ratingComment = ref('');
//
const getCategoryName = (category: string) => {
const option = categoryOptions.value.find(item => item.key === category);
return option?.label || category;
};
// 114000 => 11.4
const formatNumber = (num: number) => {
if (num >= 10000) {
return (num / 10000).toFixed(1) + '万';
}
return num.toString();
};
//
const getStarDisplay = (score: number) => {
if (!score) return '☆☆☆☆☆';
const fullStars = Math.floor(score);
const hasHalfStar = (score % 1) >= 0.25 && (score % 1) < 0.75;
const emptyStars = 5 - fullStars - (hasHalfStar ? 1 : 0);
return '★'.repeat(fullStars) + (hasHalfStar ? '⭐' : '') + '☆'.repeat(Math.max(0, emptyStars));
};
//
const getStarIcon = (value: number, currentValue: number) => {
if (value <= currentValue) {
//
if (value % 1 === 0) {
return '★';
}
//
return '⭐';
}
return '☆';
};
//
const selectRating = (value: number) => {
ratingValue.value = value;
};
//
const loadDetail = async () => {
if (!resourceId.value) {
return;
}
loading.value = true;
try {
const res = await zyGetDetailApi({ id: resourceId.value });
if (res && res.result) {
resourceDetail.value = res.result;
console.log('资源包详情:', resourceDetail.value);
console.log('资源明细列表:', resourceDetail.value.zymxList);
} else {
uni.showToast({
title: '加载失败',
icon: 'none'
});
}
} catch (error) {
console.error('加载资源包详情失败:', error);
uni.showToast({
title: '加载失败',
icon: 'none'
});
} finally {
loading.value = false;
}
};
// /
const handleCollect = async () => {
if (!resourceDetail.value) return;
try {
const res = await zyCollectApi({ zyId: resourceDetail.value.id });
if (res.resultCode === 1) {
const isCollected = resourceDetail.value.isCollect === '1';
resourceDetail.value.isCollect = isCollected ? '0' : '1';
resourceDetail.value.collNum = isCollected
? (resourceDetail.value.collNum - 1)
: (resourceDetail.value.collNum + 1);
uni.showToast({
title: isCollected ? '取消收藏成功' : '收藏成功',
icon: 'success'
});
}
} catch (error) {
console.error('收藏操作失败:', error);
uni.showToast({
title: '操作失败',
icon: 'none'
});
}
};
// /
const handleLike = async () => {
if (!resourceDetail.value) return;
try {
const res = await zyLikeApi({ zyId: resourceDetail.value.id });
if (res.resultCode === 1) {
const isLiked = resourceDetail.value.isLike === '1';
resourceDetail.value.isLike = isLiked ? '0' : '1';
resourceDetail.value.likeNum = isLiked
? (resourceDetail.value.likeNum - 1)
: (resourceDetail.value.likeNum + 1);
uni.showToast({
title: isLiked ? '取消点赞成功' : '点赞成功',
icon: 'success'
});
}
} catch (error) {
console.error('点赞操作失败:', error);
uni.showToast({
title: '操作失败',
icon: 'none'
});
}
};
//
const handleRating = () => {
ratingValue.value = resourceDetail.value?.userScore || 0;
ratingComment.value = '';
showRatingModal.value = true;
};
//
const closeRatingModal = () => {
showRatingModal.value = false;
ratingValue.value = 0;
ratingComment.value = '';
};
//
const submitRating = async () => {
if (ratingValue.value === 0) {
uni.showToast({
title: '请先评分',
icon: 'none'
});
return;
}
try {
const res = await zyScoreApi({
zyId: resourceDetail.value.id,
score: ratingValue.value,
comment: ratingComment.value
});
if (res.resultCode === 1) {
uni.showToast({
title: '评分成功',
icon: 'success'
});
closeRatingModal();
loadDetail(); //
} else {
uni.showToast({
title: '评分失败',
icon: 'none'
});
}
} catch (error) {
console.error('评分失败:', error);
uni.showToast({
title: '评分失败',
icon: 'none'
});
}
};
//
const handlePreview = (item: any) => {
const fileUrl = imagUrl(item.resourUrl);
const fileName = item.resourName + '.' + item.resSuf;
if (isVideo(item.resSuf)) {
previewVideo(fileUrl, fileName);
} else if (isImage(item.resSuf)) {
previewImage(fileUrl);
} else if (canPreview(item.resSuf)) {
previewFile(fileUrl, fileName, item.resSuf);
}
};
//
const handleDownload = (item: any) => {
const fileUrl = imagUrl(item.resourUrl);
const fileName = item.resourName + '.' + item.resSuf;
downloadFile(fileUrl, fileName)
.then(() => {
uni.showToast({
title: '下载成功',
icon: 'success'
});
})
.catch((error) => {
console.error('下载失败:', error);
});
};
//
const loadCategoryOptions = async () => {
try {
const pid = '1391443399';
const res = await dicFindByPidApi({ pid });
if (res && res.result && Array.isArray(res.result)) {
categoryOptions.value = res.result.map((item: any) => ({
key: item.dictionaryCode || item.dictionaryValue || item.id,
label: item.dictionaryName || item.dictionaryValue || item.name
}));
}
} catch (error) {
console.error('加载资源类别失败:', error);
}
};
//
const handleBack = () => {
uni.navigateBack();
};
onLoad(async (options) => {
if (options?.id) {
resourceId.value = options.id;
await loadCategoryOptions();
await loadDetail();
} else {
uni.showToast({
title: '无效的资源ID',
icon: 'none'
});
}
});
</script>
<style scoped lang="scss">
.detail-page {
min-height: 100vh;
background: #f5f5f5;
padding-bottom: 120rpx;
}
.loading-container,
.error-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 200rpx 0;
.loading-text,
.error-text {
margin-top: 20rpx;
font-size: 28rpx;
color: #999;
}
}
.detail-content {
padding: 20rpx;
}
.info-card {
background: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
.package-header {
margin-bottom: 25rpx;
.package-title {
font-size: 36rpx;
font-weight: bold;
color: #333;
line-height: 1.4;
margin-bottom: 20rpx;
display: block;
}
.package-category {
display: inline-block;
padding: 10rpx 25rpx;
background: #e6f7ff;
color: #1890ff;
border-radius: 8rpx;
font-size: 24rpx;
}
}
.rating-action-section {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx 0;
border-bottom: 1rpx solid #f0f0f0;
.rating-display {
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
.rating-score {
font-size: 48rpx;
font-weight: bold;
color: #faad14;
line-height: 1;
}
.rating-stars-wrapper {
.rating-stars {
font-size: 24rpx;
color: #faad14;
letter-spacing: 2rpx;
}
}
.rating-label {
font-size: 22rpx;
color: #999;
}
}
.action-buttons {
display: flex;
gap: 40rpx;
align-items: center;
.action-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
cursor: pointer;
padding: 10rpx;
border-radius: 8rpx;
transition: background-color 0.3s;
&:active {
background-color: #f5f5f5;
}
.like-icon {
display: flex;
align-items: center;
justify-content: center;
width: 48rpx;
height: 48rpx;
.like-emoji {
font-size: 48rpx;
line-height: 1;
}
}
.action-label {
font-size: 22rpx;
color: #666;
white-space: nowrap;
}
.action-count {
font-size: 20rpx;
color: #999;
}
}
}
}
.description-section {
padding-top: 25rpx;
.section-title {
font-size: 28rpx;
font-weight: bold;
color: #333;
margin-bottom: 15rpx;
display: block;
}
.description-text {
font-size: 26rpx;
color: #666;
line-height: 1.6;
}
}
}
.resources-card {
background: #fff;
border-radius: 16rpx;
padding: 30rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
.card-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 25rpx;
}
.resource-item {
display: flex;
align-items: center;
padding: 25rpx 0;
border-bottom: 1rpx solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.item-index {
width: 50rpx;
height: 50rpx;
background: #f0f0f0;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
color: #666;
margin-right: 20rpx;
flex-shrink: 0;
}
.item-info {
flex: 1;
min-width: 0;
.item-name {
font-size: 28rpx;
color: #333;
display: block;
margin-bottom: 10rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.item-meta {
display: flex;
gap: 15rpx;
flex-wrap: wrap;
.meta-tag {
padding: 4rpx 12rpx;
background: #f0f0f0;
border-radius: 6rpx;
font-size: 22rpx;
color: #666;
}
.meta-stats {
font-size: 22rpx;
color: #999;
}
}
}
.item-actions {
display: flex;
gap: 10rpx;
flex-shrink: 0;
margin-left: 15rpx;
}
}
.empty-state {
padding: 80rpx 0;
text-align: center;
.empty-text {
font-size: 28rpx;
color: #999;
}
}
}
.bottom-actions {
background: #fff;
padding: 20rpx;
border-top: 1rpx solid #f0f0f0;
.back-button {
width: 100%;
}
}
//
.rating-modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
//
.rating-modal {
width: 600rpx;
background: #fff;
border-radius: 16rpx;
padding: 40rpx;
margin: 0 20rpx;
.modal-title {
font-size: 36rpx;
font-weight: bold;
color: #333;
text-align: center;
margin-bottom: 30rpx;
}
.modal-content {
.resource-name {
display: block;
font-size: 28rpx;
color: #666;
margin-bottom: 25rpx;
text-align: center;
}
.rating-input {
display: flex;
flex-direction: column;
align-items: center;
gap: 20rpx;
margin-bottom: 30rpx;
.star-picker {
display: flex;
gap: 8rpx;
.star-item {
cursor: pointer;
user-select: none;
.star-icon {
font-size: 40rpx;
color: #faad14;
}
}
}
.rating-value {
font-size: 36rpx;
font-weight: bold;
color: #faad14;
}
}
.comment-textarea {
width: 100%;
min-height: 150rpx;
padding: 20rpx;
border: 1rpx solid #e0e0e0;
border-radius: 8rpx;
font-size: 28rpx;
box-sizing: border-box;
}
.char-count {
display: block;
text-align: right;
font-size: 24rpx;
color: #999;
margin-top: 10rpx;
}
}
.modal-actions {
display: flex;
gap: 20rpx;
margin-top: 30rpx;
.modal-btn {
flex: 1;
height: 80rpx;
border-radius: 8rpx;
font-size: 30rpx;
border: none;
cursor: pointer;
&.cancel-btn {
background: #f0f0f0;
color: #666;
}
&.confirm-btn {
background: #1890ff;
color: #fff;
}
}
}
}
</style>

View File

@ -0,0 +1,663 @@
<template>
<BasicListLayout
:show-nav-bar="true"
:nav-bar-props="{ title: '教学资源包' }"
@register="register"
>
<template #top>
<view class="search-section">
<view class="search-box">
<uni-icons type="search" size="18" color="#999"></uni-icons>
<input
class="search-input"
type="text"
placeholder="搜索资源包名称"
v-model="searchKeyword"
@confirm="onSearchConfirm"
/>
<view class="search-clear" v-if="searchKeyword" @click="clearSearch">
<uni-icons type="clear" size="16" color="#999"></uni-icons>
</view>
</view>
<u-button text="搜索" class="search-btn" type="primary" @click="onSearchConfirm"/>
</view>
</template>
<template #default="{ data }">
<view class="resource-package-card" @click="goToDetail(data)">
<!-- 左侧缩略图 -->
<view class="card-thumbnail">
<image
class="thumbnail-image"
src="/static/base/view/zyyl.png"
mode="aspectFit"
/>
</view>
<!-- 右侧内容 -->
<view class="card-content">
<!-- 类别标签 -->
<view class="package-category">{{ getCategoryName(data.zyCategory) }}</view>
<!-- 标题 -->
<view class="package-title">{{ data.zyName }}</view>
<!-- 资源数量 -->
<view class="package-meta" v-if="data.zymxList && data.zymxList.length > 0">
<uni-icons type="list" size="12" color="#999"></uni-icons>
<text class="meta-text">{{ data.zymxList.length }} 个资源</text>
</view>
<!-- 底部统计和评分 -->
<view class="card-footer">
<view class="stats-row">
<view class="stat-item">
<uni-icons type="eye" size="14" color="#999"></uni-icons>
<text>{{ formatNumber(data.lookNum || 0) }}</text>
</view>
<view class="stat-item">
<uni-icons
:type="data.isLike === '1' ? 'hand-thumbsup-filled' : 'hand-thumbsup'"
size="14"
:color="data.isLike === '1' ? '#1890ff' : '#999'"
></uni-icons>
<text>{{ formatNumber(data.likeNum || 0) }}</text>
</view>
</view>
<view class="rating-score">{{ (data.scoreAvg || 0).toFixed(1) }}</view>
</view>
</view>
<!-- 详情箭头 -->
<view class="card-arrow">
<uni-icons type="forward" size="20" color="#999"></uni-icons>
</view>
</view>
</template>
<!-- 底部新增按钮 -->
<template #bottom>
<view class="bottom-bar">
<u-button
text="新增资源包"
type="primary"
@click="navigateToAdd"
class="add-btn"
/>
</view>
</template>
</BasicListLayout>
<!-- 评分弹窗使用原生 modal-->
<view v-if="showRatingModal" class="rating-modal-mask" @click="closeRatingModal">
<view class="rating-modal" @click.stop>
<view class="modal-title">资源评分</view>
<view class="modal-content">
<text class="resource-name">{{ currentResource?.zyName }}</text>
<view class="rating-input">
<view class="star-picker">
<view
v-for="star in 10"
:key="star"
class="star-item"
@click="selectRating(star / 2)"
>
<text class="star-icon">{{ getStarIcon(star / 2, ratingValue) }}</text>
</view>
</view>
<text class="rating-value">{{ ratingValue.toFixed(1) }}</text>
</view>
<textarea
v-model="ratingComment"
placeholder="请输入评价内容(选填)"
maxlength="200"
class="comment-textarea"
></textarea>
<text class="char-count">{{ ratingComment.length }}/200</text>
</view>
<view class="modal-actions">
<button class="modal-btn cancel-btn" @click="closeRatingModal">取消</button>
<button class="modal-btn confirm-btn" @click="submitRating">确认</button>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { useLayout } from "@/components/BasicListLayout/hooks/useLayout";
import {
zyFindPageApi,
zyCollectApi,
zyLikeApi,
zyScoreApi,
dicFindByPidApi
} from "@/api/base/server";
import { useDataStore } from "@/store/modules/data";
const { getData } = useDataStore();
const searchKeyword = ref<string>('');
const categoryOptions = ref<any[]>([]);
const showRatingModal = ref(false);
const currentResource = ref<any>(null);
const ratingValue = ref(0);
const ratingComment = ref('');
// Hook
const [register, { reload, setParam }] = useLayout({
api: zyFindPageApi,
componentProps: {
defaultPageSize: 10,
loadingMoreEnabled: true,
},
});
//
const buildParams = () => {
const params = {
...getData,
zyName: searchKeyword.value,
status: 'A'
};
console.log('查询资源包参数:', params);
setParam(params);
reload();
};
//
const onSearchConfirm = () => {
buildParams();
};
//
const clearSearch = () => {
searchKeyword.value = '';
buildParams();
};
//
const getCategoryName = (category: string) => {
const option = categoryOptions.value.find(item => item.key === category);
return option?.label || category;
};
//
const formatNumber = (num: number) => {
if (num >= 10000) {
return (num / 10000).toFixed(1) + '万';
}
return num.toString();
};
//
const getStarDisplay = (score: number) => {
if (!score) return '☆☆☆☆☆';
const fullStars = Math.floor(score);
const hasHalfStar = (score % 1) >= 0.25 && (score % 1) < 0.75;
const emptyStars = 5 - fullStars - (hasHalfStar ? 1 : 0);
return '★'.repeat(fullStars) + (hasHalfStar ? '⭐' : '') + '☆'.repeat(Math.max(0, emptyStars));
};
//
const getStarIcon = (value: number, currentValue: number) => {
if (value <= currentValue) {
//
if (value % 1 === 0) {
return '★';
}
//
return '⭐';
}
return '☆';
};
//
const selectRating = (value: number) => {
ratingValue.value = value;
};
// /
const handleCollect = async (item: any) => {
try {
const res = await zyCollectApi({ zyId: item.id });
if (res.resultCode === 1) {
//
const isCollected = item.isCollect === '1';
item.isCollect = isCollected ? '0' : '1';
item.collNum = isCollected ? (item.collNum - 1) : (item.collNum + 1);
uni.showToast({
title: isCollected ? '取消收藏成功' : '收藏成功',
icon: 'success'
});
} else {
uni.showToast({
title: '操作失败',
icon: 'none'
});
}
} catch (error) {
console.error('收藏操作失败:', error);
uni.showToast({
title: '操作失败',
icon: 'none'
});
}
};
// /
const handleLike = async (item: any) => {
try {
const res = await zyLikeApi({ zyId: item.id });
if (res.resultCode === 1) {
//
const isLiked = item.isLike === '1';
item.isLike = isLiked ? '0' : '1';
item.likeNum = isLiked ? (item.likeNum - 1) : (item.likeNum + 1);
uni.showToast({
title: isLiked ? '取消点赞成功' : '点赞成功',
icon: 'success'
});
} else {
uni.showToast({
title: '操作失败',
icon: 'none'
});
}
} catch (error) {
console.error('点赞操作失败:', error);
uni.showToast({
title: '操作失败',
icon: 'none'
});
}
};
//
const handleRating = (item: any) => {
currentResource.value = item;
ratingValue.value = item.userScore || 0;
ratingComment.value = '';
showRatingModal.value = true;
};
//
const closeRatingModal = () => {
showRatingModal.value = false;
currentResource.value = null;
ratingValue.value = 0;
ratingComment.value = '';
};
//
const submitRating = async () => {
if (ratingValue.value === 0) {
uni.showToast({
title: '请先评分',
icon: 'none'
});
return;
}
try {
const res = await zyScoreApi({
zyId: currentResource.value.id,
score: ratingValue.value,
comment: ratingComment.value
});
if (res.resultCode === 1) {
uni.showToast({
title: '评分成功',
icon: 'success'
});
closeRatingModal();
reload(); //
} else {
uni.showToast({
title: '评分失败',
icon: 'none'
});
}
} catch (error) {
console.error('评分失败:', error);
uni.showToast({
title: '评分失败',
icon: 'none'
});
}
};
//
const goToDetail = (item: any) => {
uni.navigateTo({
url: `/pages/view/routine/JiaoXueZiYuan/zy-detail?id=${item.id}`
});
};
//
const navigateToAdd = () => {
uni.navigateTo({
url: '/pages/view/routine/JiaoXueZiYuan/zy-add'
});
};
//
const loadCategoryOptions = async () => {
try {
const pid = '1391443399'; // ID
const res = await dicFindByPidApi({ pid });
if (res && res.result && Array.isArray(res.result)) {
categoryOptions.value = res.result.map((item: any) => ({
key: item.dictionaryCode || item.dictionaryValue || item.id,
label: item.dictionaryName || item.dictionaryValue || item.name
}));
}
} catch (error) {
console.error('加载资源类别失败:', error);
}
};
//
const refreshList = () => {
console.log('收到刷新事件,重新加载资源包列表');
reload();
};
onMounted(() => {
loadCategoryOptions();
buildParams();
//
uni.$on('refreshResourceList', refreshList);
});
onUnmounted(() => {
//
uni.$off('refreshResourceList', refreshList);
});
</script>
<style scoped lang="scss">
.search-section {
padding: 20rpx;
background: #fff;
display: flex;
align-items: center;
gap: 15rpx;
.search-box {
flex: 1;
display: flex;
align-items: center;
background: #f5f5f5;
border-radius: 30rpx;
padding: 15rpx 25rpx;
.search-input {
flex: 1;
margin-left: 15rpx;
font-size: 28rpx;
color: #333;
}
.search-clear {
margin-left: 10rpx;
padding: 5rpx;
}
}
.search-btn {
width: 120rpx;
height: 60rpx;
}
}
.resource-package-card {
background: #fff;
margin: 15rpx 10rpx;
border-radius: 12rpx;
padding: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
display: flex;
gap: 20rpx;
position: relative;
//
.card-thumbnail {
flex-shrink: 0;
width: 110rpx;
height: 90rpx;
border-radius: 8rpx;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background: #f5f5f5;
.thumbnail-image {
width: 100%;
height: 100%;
}
}
//
.card-content {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
min-width: 0;
padding-right: 50rpx; //
.package-category {
display: inline-block;
align-self: flex-start;
padding: 4rpx 16rpx;
background: #1890ff;
color: #fff;
border-radius: 4rpx;
font-size: 22rpx;
margin-bottom: 8rpx;
}
.package-title {
font-size: 30rpx;
font-weight: bold;
color: #333;
line-height: 1.4;
margin-bottom: 8rpx;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
}
.package-meta {
display: flex;
align-items: center;
gap: 6rpx;
margin-bottom: auto; //
.meta-text {
font-size: 22rpx;
color: #999;
}
}
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10rpx;
.stats-row {
display: flex;
gap: 25rpx;
.stat-item {
display: flex;
align-items: center;
gap: 6rpx;
font-size: 24rpx;
color: #666;
}
}
.rating-score {
font-size: 28rpx;
font-weight: bold;
color: #ff8800;
}
}
}
//
.card-arrow {
position: absolute;
right: 20rpx;
top: 50%;
transform: translateY(-50%);
display: flex;
align-items: center;
justify-content: center;
}
}
//
.bottom-bar {
padding: 20rpx;
background: #fff;
border-top: 1rpx solid #f0f0f0;
.add-btn {
width: 100%;
}
}
//
.rating-modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
//
.rating-modal {
width: 600rpx;
background: #fff;
border-radius: 16rpx;
padding: 40rpx;
margin: 0 20rpx;
.modal-title {
font-size: 36rpx;
font-weight: bold;
color: #333;
text-align: center;
margin-bottom: 30rpx;
}
.modal-content {
.resource-name {
display: block;
font-size: 28rpx;
color: #666;
margin-bottom: 25rpx;
text-align: center;
}
.rating-input {
display: flex;
flex-direction: column;
align-items: center;
gap: 20rpx;
margin-bottom: 30rpx;
.star-picker {
display: flex;
gap: 8rpx;
.star-item {
cursor: pointer;
user-select: none;
.star-icon {
font-size: 40rpx;
color: #faad14;
}
}
}
.rating-value {
font-size: 36rpx;
font-weight: bold;
color: #faad14;
}
}
.comment-textarea {
width: 100%;
min-height: 150rpx;
padding: 20rpx;
border: 1rpx solid #e0e0e0;
border-radius: 8rpx;
font-size: 28rpx;
box-sizing: border-box;
}
.char-count {
display: block;
text-align: right;
font-size: 24rpx;
color: #999;
margin-top: 10rpx;
}
}
.modal-actions {
display: flex;
gap: 20rpx;
margin-top: 30rpx;
.modal-btn {
flex: 1;
height: 80rpx;
border-radius: 8rpx;
font-size: 30rpx;
border: none;
cursor: pointer;
&.cancel-btn {
background: #f0f0f0;
color: #666;
}
&.confirm-btn {
background: #1890ff;
color: #fff;
}
}
}
}
</style>

View File

@ -57,18 +57,18 @@
<text style="margin-right: 8px;">{{ idx + 1 }}{{ xm.xcMc }}</text>
<view style="display: flex; align-items: center;">
<u-icon
:name="xm.xcJg === 'B' ? 'checkmark-circle-fill' : 'close-circle-fill'"
:color="xm.xcJg === 'B' ? '#67c23a' : '#f56c6c'"
:name="xm.xcJg === 'A' ? 'checkmark-circle-fill' : 'close-circle-fill'"
:color="xm.xcJg === 'A' ? '#67c23a' : '#f56c6c'"
size="18"
></u-icon>
</view>
</view>
<view style="font-size: 12px; color: #666;">
<text v-if="xm.xcJg === 'A'" style="color: #f56c6c;">
扣分-{{ xm.xmFz }}
<text v-if="xm.xcJg === 'A'" style="color: #67c23a;">
优点
</text>
<text v-else style="color: #67c23a;">
不扣分
<text v-else style="color: #f56c6c;">
缺点
</text>
</view>
</view>

View File

@ -55,18 +55,18 @@
<text style="margin-right: 8px;">{{ idx + 1 }}{{ xm.xcMc }}</text>
<view style="display: flex; align-items: center;">
<u-icon
:name="xm.xcJg === 'B' ? 'checkmark-circle-fill' : 'close-circle-fill'"
:color="xm.xcJg === 'B' ? '#67c23a' : '#f56c6c'"
:name="xm.xcJg === 'A' ? 'checkmark-circle-fill' : 'close-circle-fill'"
:color="xm.xcJg === 'A' ? '#67c23a' : '#f56c6c'"
size="18"
></u-icon>
</view>
</view>
<view style="font-size: 12px; color: #666;">
<text v-if="xm.xcJg === 'A'" style="color: #f56c6c;">
扣分-{{ xm.xmFz }}
<text v-if="xm.xcJg === 'A'" style="color: #67c23a;">
优点
</text>
<text v-else style="color: #67c23a;">
不扣分
<text v-else style="color: #f56c6c;">
缺点
</text>
</view>
</view>

View File

@ -85,7 +85,7 @@ import { useDataStore } from "@/store/modules/data";
import { getPbPageApi } from "@/api/base/pbApi";
import dayjs from "dayjs";
const { getJs } = useUserStore();
const { getJs, getUser } = useUserStore();
const { getData, setData } = useDataStore();
//
@ -118,7 +118,7 @@ const loadPbList = async (isRefresh = false) => {
}
try {
const params = {
const params: any = {
page: pagination.page,
rows: pagination.pageSize,
xclx: 'C', // C-
@ -128,6 +128,30 @@ const loadPbList = async (isRefresh = false) => {
// xqId: '', // ID
};
// admin ID
const user = getUser;
const js = getJs;
const empCode = user?.empCode || '';
//
console.log('=== 值周巡查权限判断 ===');
console.log('getUser:', user);
console.log('getUser.empCode:', user?.empCode);
console.log('empCode:', empCode);
console.log('getJs:', js);
console.log('getJs.id:', js?.id);
if (empCode === 'admin') {
// admin
console.log('权限admin 用户,查看所有排班');
} else {
// admin dqjsId使 FIND_IN_SET
params.dqjsId = js?.id; // ID
console.log('权限:普通用户,过滤条件 dqjsId =', params.dqjsId);
}
console.log('最终请求参数:', params);
console.log('=========================');
const res: any = await getPbPageApi(params);
// API

View File

@ -53,6 +53,20 @@
</view>
<view v-if="canInspect">
<!-- 巡查年级班级 -->
<view class="section mx-15 mb-15">
<view class="section-title-bar">
<view class="decorator"></view>
<text class="title-text">巡查年级班级</text>
</view>
<view class="class-select-card bg-white r-md p-15">
<view class="class-selector" @click="showClassTree">
<text :class="{ placeholder: !selectedClassText }">{{ selectedClassText || "请选择年级班级" }}</text>
<uni-icons type="right" size="16" color="#999"></uni-icons>
</view>
</view>
</view>
<!-- 巡查项目 -->
<view class="section mx-15 mb-15">
<view class="section-title-bar">
@ -160,7 +174,7 @@
:disabled="isSubmitting"
@click="submit"
>
{{ isSubmitting ? (xcRecordId ? '更新中...' : '提交中...') : (xcRecordId ? '更新巡查' : '提交巡查') }}
{{ isSubmitting ? '提交中...' : '提交巡查' }}
</button>
</view>
</template>
@ -198,6 +212,19 @@
</view>
</view>
</view>
<!-- 班级选择树 -->
<BasicTree
ref="treeRef"
:range="treeData"
idKey="key"
rangeKey="title"
title="选择年级班级"
:multiple="false"
:selectParent="false"
@confirm="onTreeConfirm"
@cancel="onTreeCancel"
/>
</template>
<script setup lang="ts">
@ -206,6 +233,8 @@ import { zbXcSaveApi } from "@/api/base/zbXcApi";
import { attachmentUpload } from "@/api/system/upload";
import BasicLayout from "@/components/BasicLayout/Layout.vue";
import { ImageVideoUpload } from "@/components/ImageVideoUpload";
import BasicTree from '@/components/BasicTree/Tree.vue';
import { findAllNjBjTree } from '@/api/base/server';
import { useDataStore } from "@/store/modules/data";
import { useUserStore } from "@/store/modules/user";
import { computed, onMounted, ref } from "vue";
@ -233,8 +262,19 @@ const todayInfo = ref({
//
const checkItems = ref<any[]>([]);
// ID
const xcRecordId = ref('');
//
const treeData = ref<any[]>([]);
const treeRef = ref();
const curNj = ref<any>(null);
const curBj = ref<any>(null);
//
const selectedClassText = computed(() => {
if (curBj.value && curNj.value) {
return `${curNj.value.title} ${curBj.value.title}`;
}
return '';
});
//
import { type ImageItem, type VideoItem, COMPRESS_PRESETS } from '@/components/ImageVideoUpload'
@ -265,6 +305,60 @@ const formatTime = (timestamp: string) => {
return dayjs(timestamp).format('MM-DD');
};
//
const loadTreeData = async () => {
try {
const res = await findAllNjBjTree();
if (res.resultCode === 1 && res.result) {
// BasicTree
const convertTreeData = (items: any[]): any[] => {
return items.map((item: any) => ({
key: item.key,
title: item.title,
njmcId: item.njmcId,
children: item.children ? convertTreeData(item.children) : [],
}));
};
treeData.value = convertTreeData(res.result);
}
} catch (error) {
uni.showToast({ title: "加载班级数据失败", icon: "error" });
}
};
//
const showClassTree = () => {
if (treeRef.value) {
treeRef.value._show();
}
};
//
const onTreeConfirm = (selectedItems: any[]) => {
if (selectedItems.length > 0) {
const selectedItem = selectedItems[0]; //
// parents
if (selectedItem.parents && selectedItem.parents.length > 0) {
const parent = selectedItem.parents[0]; //
const nj = parent; //
const bj = selectedItem; //
curNj.value = nj;
curBj.value = bj;
} else {
//
curNj.value = selectedItem;
curBj.value = null;
}
}
};
//
const onTreeCancel = () => {
//
};
//
const loadCheckItems = async () => {
try {
@ -289,11 +383,6 @@ const loadCheckItems = async () => {
};
});
console.log('巡查项目列表:', checkItems.value);
//
if (zb.value.xcRecord) {
restoreXcRecord(zb.value.xcRecord);
}
} else {
checkItems.value = [];
}
@ -303,85 +392,6 @@ const loadCheckItems = async () => {
}
};
//
const restoreXcRecord = (xcRecord: any) => {
try {
console.log('========== 开始回显巡查记录 ==========');
console.log('巡查记录ID:', xcRecord.id);
console.log('巡查记录 zbXcXmList:', xcRecord.zbXcXmList);
console.log('当前巡查项目列表:', checkItems.value.map(item => ({
id: item.id,
xcMc: item.xcMc
})));
// ID
xcRecordId.value = xcRecord.id || '';
console.log('保存的巡查记录ID:', xcRecordId.value);
//
if (xcRecord.zbXcXmList && xcRecord.zbXcXmList.length > 0) {
console.log('开始回显', xcRecord.zbXcXmList.length, '个巡查项目');
let matchedCount = 0;
xcRecord.zbXcXmList.forEach((xcXm: any, index: number) => {
console.log(`\n[${index + 1}] 尝试匹配项目:`);
console.log(' xcXm.id:', xcXm.id);
console.log(' xcXm.xcXmId:', xcXm.xcXmId);
console.log(' xcXm.xcMc:', xcXm.xcMc);
console.log(' xcXm.xcJg:', xcXm.xcJg);
console.log(' xcXm.xcPj:', xcXm.xcPj);
const checkItem = checkItems.value.find(item => item.id === xcXm.xcXmId);
if (checkItem) {
console.log(' ✓ 找到匹配项目:', checkItem.xcMc);
checkItem.xcJg = xcXm.xcJg || '';
checkItem.xcPj = xcXm.xcPj || '';
checkItem.xcXmRecordId = xcXm.id || ''; // ID
matchedCount++;
console.log(' 设置后 - xcJg:', checkItem.xcJg, ', xcPj:', checkItem.xcPj);
} else {
console.log(' ✗ 未找到匹配项目!');
console.log(' 可用的项目ID:', checkItems.value.map(item => item.id).join(', '));
}
});
console.log(`\n成功匹配 ${matchedCount}/${xcRecord.zbXcXmList.length} 个巡查项目`);
} else {
console.log('⚠️ 没有巡查项目记录需要回显zbXcXmList 为空或不存在)');
}
//
if (xcRecord.zp) {
const imageUrls = xcRecord.zp.split(',').map((url: string) => url.trim()).filter((url: string) => url);
imageList.value = imageUrls.map((url: string) => ({
url: url,
path: imagUrl(url),
uploaded: true
}));
console.log('✓ 回显图片:', imageList.value.length, '张');
}
//
if (xcRecord.sp) {
const videoUrls = xcRecord.sp.split(',').map((url: string) => url.trim()).filter((url: string) => url);
videoList.value = videoUrls.map((url: string) => ({
url: url,
path: imagUrl(url),
uploaded: true
}));
console.log('✓ 回显视频:', videoList.value.length, '个');
}
console.log('\n回显完成最终巡查项目状态:');
checkItems.value.forEach((item, index) => {
console.log(` [${index + 1}] ${item.xcMc} - xcJg: ${item.xcJg || '未选择'}, xcPj: ${item.xcPj || '无'}`);
});
console.log('========== 回显完成 ==========\n');
} catch (error) {
console.error('❌ 回显巡查记录失败:', error);
}
};
const onCheckItemChange = (e: any, item: any) => {
const value = e.detail.value; // 'A' 'B'
const label = value === 'A' ? '优点' : '缺点';
@ -506,6 +516,16 @@ const submit = async () => {
return;
}
//
if (!curBj.value || !curNj.value) {
uni.showToast({
title: "请选择巡查的年级班级",
icon: "none",
duration: 2000,
});
return;
}
//
const hasCheckedItems = checkItems.value.some(item => item.xcJg && item.xcPj);
if (!hasCheckedItems) {
@ -524,20 +544,13 @@ const submit = async () => {
const zbXcXmList = checkItems.value
.filter((item: any) => item.xcJg && item.xcPj)
.map((item: any) => {
const newItem = {
...item,
return {
xcXmId: item.id,
xcJg: item.xcJg, // A-B-
xcPj: item.xcPj, //
zbXcId: xcRecordId.value || "", // ID使
xmFz: item.xmFz, //
xcMc: item.xcMc, //
};
// IDID
if (xcRecordId.value && item.xcXmRecordId) {
newItem.id = item.xcXmRecordId;
} else {
newItem.id = "";
}
return newItem;
});
const submitData: any = {
@ -548,21 +561,20 @@ const submit = async () => {
xctime: now.format("YYYY-MM-DD HH:mm:ss"),
zp: getImageUrls(),
sp: getVideoUrls(),
njId: curNj.value.key, // ID
njmcId: curNj.value.njmcId || curNj.value.key, // ID
bjId: curBj.value.key, // ID
bc: selectedClassText.value, // 1
zbXcXmList: zbXcXmList,
};
// ID
if (xcRecordId.value) {
submitData.id = xcRecordId.value;
}
console.log('提交值周巡查数据:', submitData);
const res = await zbXcSaveApi(submitData);
if (res && res.resultCode === 1) {
uni.showToast({
title: xcRecordId.value ? "更新成功" : "提交成功",
title: "提交成功",
icon: "success",
});
setTimeout(() => {
@ -575,14 +587,14 @@ const submit = async () => {
}, 1500);
} else {
uni.showToast({
title: xcRecordId.value ? "更新失败" : "提交失败",
title: "提交失败",
icon: "none",
});
}
} catch (error) {
console.error('提交值周巡查失败:', error);
uni.showToast({
title: xcRecordId.value ? "更新失败" : "提交失败",
title: "提交失败",
icon: "none",
});
} finally {
@ -613,6 +625,7 @@ const getVideoUrls = () => {
//
onMounted(async () => {
await loadCheckItems();
await loadTreeData(); //
checkInspectionTime();
});
</script>
@ -819,6 +832,35 @@ onMounted(async () => {
}
}
.class-select-card {
background-color: white;
}
.class-selector {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 15px;
background-color: #f9f9f9;
border-radius: 6px;
border: 1px solid #eee;
cursor: pointer;
transition: all 0.3s ease;
text {
font-size: 14px;
color: #333;
&.placeholder {
color: #999;
}
}
&:active {
background-color: #f0f0f0;
}
}
.upload-card {
background-color: white;
}

View File

@ -79,13 +79,12 @@
<script setup lang="ts">
import { getZbXcListApi } from "@/api/base/pbApi";
import { zbXcFindPageApi } from "@/api/base/zbXcApi";
import { useDataStore } from "@/store/modules/data";
import { useUserStore } from "@/store/modules/user";
import { onBeforeUnmount, onMounted, ref } from "vue";
import dayjs from "dayjs";
const { getJs } = useUserStore();
const { getJs, getUser } = useUserStore();
const dataStore = useDataStore();
//
@ -214,16 +213,34 @@ const loadZbXcList = async (pbData: any) => {
const pbId = pbData.pbId;
// admin dqjsId
const user = getUser;
const empCode = user?.empCode || '';
//
const apiParams: any = {
pbId: pbId
};
//
if (empCode === 'admin') {
// admin jsId
apiParams.jsId = '';
apiParams.qdjsId = getJs.id;
console.log('权限admin 用户,传递 jsId');
} else {
// admin dqjsId
apiParams.jsId = getJs.id;
apiParams.qdjsId = '';
console.log('权限:普通用户,传递 dqjsId 进行权限过滤');
}
console.log('API调用参数:', {
jsId: getJs.id,
pbId: pbId,
...apiParams,
pbData: pbData
});
const res = await getZbXcListApi({
jsId: getJs.id,
pbId: pbId
});
const res = await getZbXcListApi(apiParams);
if (res && res.resultCode == 1) {
const list = res.result || [];
@ -295,8 +312,8 @@ const refreshZbList = async () => {
}
};
//
const goXc = async (zb: any) => {
//
const goXc = (zb: any) => {
const pbData = dataStore.getGlobal;
//
@ -318,68 +335,6 @@ const goXc = async (zb: any) => {
xqmc: pbData.xqmc
};
//
if (zb.sfxc === '是') {
try {
uni.showLoading({
title: '加载中...'
});
console.log('========== 查询已巡查记录 ==========');
console.log('查询参数 pbZbId:', zb.id);
const res = await zbXcFindPageApi({
rows: 10,
page: 1,
pbZbId: zb.id
});
console.log('查询结果完整数据:', res);
console.log('查询结果 resultCode:', res?.resultCode);
console.log('查询结果 result 数量:', res?.result?.length);
console.log('查询结果 rows:', res?.rows);
console.log('查询结果 rows 数量:', res?.rows?.length);
uni.hideLoading();
//
let xcRecordList = res?.result || res?.rows || [];
if (res && (res.resultCode === 1 || res.resultCode === '1' || xcRecordList.length > 0)) {
if (xcRecordList.length > 0) {
//
const xcRecord = xcRecordList[0];
combinedData.xcRecord = xcRecord;
console.log('✓ 获取到巡查记录ID:', xcRecord.id);
console.log(' - jsxm:', xcRecord.jsxm);
console.log(' - xctime:', xcRecord.xctime);
console.log(' - zbXcXmList 数量:', xcRecord.zbXcXmList?.length || 0);
if (xcRecord.zbXcXmList && xcRecord.zbXcXmList.length > 0) {
console.log(' - zbXcXmList 详情:', xcRecord.zbXcXmList.map((xm: any) => ({
xcXmId: xm.xcXmId,
xcMc: xm.xcMc,
xcJg: xm.xcJg,
xcPj: xm.xcPj
})));
} else {
console.log(' ⚠️ zbXcXmList 为空!');
}
console.log('========== 查询完成 ==========');
} else {
console.log('❌ 返回数据为空');
}
} else {
console.log('❌ 未查询到巡查记录或查询失败');
console.log(' 返回数据结构:', Object.keys(res || {}));
}
} catch (error) {
uni.hideLoading();
console.error('查询巡查记录失败:', error);
}
} else {
console.log('该值周记录尚未巡查,无需回显');
}
console.log('点击巡查,传递数据:', combinedData);
dataStore.setData(combinedData);

View File

@ -63,18 +63,18 @@
<text style="margin-right: 8px;">{{ idx + 1 }}{{ xm.xcMc }}</text>
<view style="display: flex; align-items: center;">
<u-icon
:name="xm.xcJg === 'B' ? 'checkmark-circle-fill' : 'close-circle-fill'"
:color="xm.xcJg === 'B' ? '#67c23a' : '#f56c6c'"
:name="xm.xcJg === 'A' ? 'checkmark-circle-fill' : 'close-circle-fill'"
:color="xm.xcJg === 'A' ? '#67c23a' : '#f56c6c'"
size="18"
></u-icon>
</view>
</view>
<view style="font-size: 12px; color: #666;">
<text v-if="xm.xcJg === 'A'" style="color: #f56c6c;">
扣分-{{ xm.xmFz }}
<text v-if="xm.xcJg === 'A'" style="color: #67c23a;">
优点
</text>
<text v-else style="color: #67c23a;">
不扣分
<text v-else style="color: #f56c6c;">
缺点
</text>
</view>
</view>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB