新增食堂巡查

This commit is contained in:
hb 2025-07-21 19:49:03 +08:00
parent 87d90244aa
commit ee5e5c335f
10 changed files with 1408 additions and 54 deletions

30
src/api/base/hcApi.ts Normal file
View File

@ -0,0 +1,30 @@
// 食堂巡查相关API接口
import { get, post } from "@/utils/request";
/**
*
*/
export function hcFindPageApi(params: any) {
return get("/api/hc/findPage", params);
}
/**
* /
*/
export function hcSaveApi(params: any) {
return post("/api/hc/save", params);
}
/**
* id查询食堂巡查记录
*/
export function hcFindByIdApi(params: any) {
return get("/api/hc/findById", params);
}
/**
*
*/
export function hcLogicDeleteApi(params: any) {
return post("/api/hc/logicDelete", params);
}

View File

@ -181,6 +181,27 @@
"enablePullDownRefresh": false
}
},
{
"path": "pages/view/routine/ShiTangXunCha/index",
"style": {
"navigationBarTitleText": "食堂巡查",
"enablePullDownRefresh": false
}
},
{
"path": "pages/view/routine/ShiTangXunCha/add",
"style": {
"navigationBarTitleText": "新增食堂巡查",
"enablePullDownRefresh": false
}
},
{
"path": "pages/view/routine/ShiTangXunCha/detail",
"style": {
"navigationBarTitleText": "食堂巡查详情",
"enablePullDownRefresh": false
}
},
{
"path": "pages/view/routine/JiFenPingJia/detail",
"style": {

View File

@ -181,6 +181,7 @@ const sections = reactive<Section[]>([
show: true,
path: "/pages/view/routine/GongZuoLiang/index",
},
// {
// id: "r4",
// icon: "file-paper-2-fill",
@ -195,7 +196,13 @@ const sections = reactive<Section[]>([
show: true,
path: "/pages/view/routine/RengJiaoRengZhi/index",
},
{
id: "r9",
icon: "hc-fill",
text: "食堂巡查",
show: true,
path: "/pages/view/routine/ShiTangXunCha/index",
},
{
id: "r6",
icon: "pass-pending-fill",
@ -252,13 +259,13 @@ const sections = reactive<Section[]>([
show: true,
path: "/pages/view/homeSchool/parentAddressBook/index",
},
{
/*{
id: "hs4",
icon: "newspaper-fill",
text: "通知列表",
show: true,
path: "/pages/view/notice/index",
},
},*/
{
id: "hs6",
icon: "filechart2fil",

View File

@ -207,7 +207,7 @@ watch(studentList, (list) => {
<style scoped lang="scss">
.notice-detail-page {
background-color: #f4f5f7;
min-height: 100vh;
padding-bottom: 70px;
padding: 15px;
box-sizing: border-box;
}

View File

@ -55,7 +55,7 @@
type="primary"
@click="goToPublish"
/>
</view>
</view>
</template>
</BasicListLayout>
<!-- 用uniqueList渲染 -->

View File

@ -130,6 +130,21 @@
</picker>
</view>
<!-- 回执说明 -->
<view class="info-card" v-if="formData.signatureRequired">
<view class="form-item">
<text class="form-label">回执说明</text>
<uni-easyinput
type="textarea"
autoHeight
v-model="formData.mdhz"
placeholder="请输入回执说明"
:inputBorder="false"
class="receipt-input"
></uni-easyinput>
</view>
</view>
<view class="info-card list-item-card">
<uni-datetime-picker type="datetime" v-model="formData.startTime">
<view class="list-item-row">
@ -160,9 +175,6 @@
<button class="action-btn draft-btn" @click="saveDraft">
保存草稿
</button>
<button class="action-btn preview-btn" @click="previewNotice">
预览
</button>
<button class="action-btn publish-btn" @click="publishNotice" :disabled="isPublishing">
{{ isPublishing ? '发布中...' : '立即发布' }}
</button>
@ -222,7 +234,7 @@
</template>
<script lang="ts" setup>
import { ref, reactive, computed, nextTick } from "vue";
import { ref, reactive, computed, nextTick, watch } from "vue";
import { onLoad, onShow } from "@dcloudio/uni-app";
import CustomUpload from "/src/components/BasicUpload/CustomUpload.vue";
import BasicTree from "@/components/BasicTree/Tree.vue";
@ -254,6 +266,7 @@ interface FormData {
signatureRequired: boolean;
startTime: string;
endTime: string;
mdhz: string; //
}
const noticeId = ref<string | null>(null);
@ -273,6 +286,7 @@ const formData = reactive<FormData>({
signatureRequired: true,
startTime: "",
endTime: "",
mdhz: "", //
});
const signatureOptions = ["启用" , "不启用"];
@ -281,7 +295,7 @@ const signatureStatusText = computed(() => {
});
//
const treeData = ref([]);
const treeData = ref<any[]>([]);
const treeRef = ref();
//
@ -296,14 +310,23 @@ const displayNames = computed(() => {
// ref
const coverUploadRef = ref();
//
interface ApiResponse<T = any> {
resultCode: number;
result?: T;
resultMsg?: string;
data?: any;
[key: string]: any;
}
//
const loadTreeData = async () => {
try {
const res = await findAllNjBjTree();
if (res.resultCode === 1 && res.result) {
const res = await findAllNjBjTree() as ApiResponse<any[]>;
if (res && res.resultCode === 1 && res.result) {
// BasicTree njmcId
const convertTreeData = (items: any[]): any[] => {
return items.map((item: any) => ({
const convertTreeData = (items: Array<{ key: string; title: string; njmcId?: string; children?: any[] }>): any[] => {
return items.map((item) => ({
key: item.key,
title: item.title,
njmcId: item.njmcId, // njmcId
@ -355,9 +378,10 @@ const resetForm = () => {
targetNjIds: [],
targetNjmcIds: [],
targetBjIds: [],
signatureRequired: false,
signatureRequired: true, //
startTime: "",
endTime: "",
mdhz: "", //
});
//
@ -384,9 +408,7 @@ const resetForm = () => {
const loadJlData = async (jlId: string) => {
try {
uni.showLoading({ title: "加载数据中..." });
//
const response = await jlFindByIdApi({ id: jlId });
const response = await jlFindByIdApi({ id: jlId }) as ApiResponse<any>;
if (response && response.resultCode === 1 && response.result) {
const jlData = response.result;
@ -399,14 +421,15 @@ const loadJlData = async (jlId: string) => {
formData.startTime = jlData.jlkstime || "";
formData.endTime = jlData.jljstime || "";
formData.signatureRequired = jlData.mdqz === "1";
formData.mdhz = jlData.mdhz || ""; //
//
if (jlData.jlfj) {
const attachmentUrls = jlData.jlfj.split(",");
formData.attachments = attachmentUrls.map(url => ({
formData.attachments = attachmentUrls.map((url: string) => ({
name: url.split("/").pop() || "附件",
type: "file",
url: url
url: url as string
}));
}
@ -573,7 +596,7 @@ const getAttachmentIcon = (type: string): string => {
const previewAttachment = (attachment: Attachment) => {
//
if (attachment.type === "image") {
const fullUrl = imagUrl(attachment.url);
const fullUrl = imagUrl(attachment.url as string);
uni.previewImage({
urls: [fullUrl],
current: fullUrl,
@ -594,11 +617,11 @@ const showClassTree = () => {
};
//
const onTreeConfirm = async (selectedItems: any[]) => {
const onTreeConfirm = async (selectedItems: Array<any>) => {
if (selectedItems.length > 0) {
//
const classNames = selectedItems.map((item) => item.title);
const classNames = selectedItems.map((item: any) => item.title);
formData.targetClass = classNames.join(", ");
formData.targetNames = [];
formData.targetStudentIds = [];
@ -615,16 +638,24 @@ const onTreeConfirm = async (selectedItems: any[]) => {
const bjIds: string[] = [];
const gradeNames: string[] = []; //
selectedItems.forEach((item) => {
selectedItems.forEach((item: any) => {
// parents
if (item.parents && item.parents.length > 0) {
const parent = item.parents[0]; //
const njId = parent.key; // IDparents[0].key
const njmcId = parent.njmcId; // ID
const bjId = item.key; // IDitem.key
const gradeName = parent.title; //
// keytreeDatanjmcId
let njmcId: string | undefined;
for (const grade of treeData.value || []) {
if (grade.key === njId) {
njmcId = grade.njmcId;
break;
}
}
//
if (njId && bjId && gradeName) {
njIds.push(njId);
@ -669,7 +700,7 @@ const onTreeConfirm = async (selectedItems: any[]) => {
bjId: bjIds.join(","),
};
const response = await mobilejlstudentListApi(params);
const response = await mobilejlstudentListApi(params) as ApiResponse<any[]>;
if (response && response.resultCode === 1 && response.result) {
//
@ -699,7 +730,6 @@ const onTreeConfirm = async (selectedItems: any[]) => {
icon: "success",
});
} catch (error) {
console.error("获取学生列表失败:", error);
uni.hideLoading();
uni.showToast({ title: "获取学生列表失败", icon: "error" });
@ -733,7 +763,16 @@ const removeStudent = (index: number) => {
const handleSignatureChange = (e: any) => {
const index = e.detail.value;
formData.signatureRequired = index == 1;
formData.signatureRequired = index == 0; // 0""1""
//
if (formData.signatureRequired) {
const title = formData.title.trim() || "通知";
formData.mdhz = `我已认真阅读《${title}》内容,加强对孩子安全教育和安全监管,对孩子的安全全面负责。`;
} else {
//
formData.mdhz = "";
}
};
const validateForm = () => {
@ -814,6 +853,7 @@ const buildJlDto = (status: string) => {
jlkstime: formatDate(formData.startTime), // yyyy-MM-dd HH:mm:ss
jljstime: formatDate(formData.endTime), // yyyy-MM-dd HH:mm:ss
mdqz: formData.signatureRequired ? "1" : "0", // 10
mdhz: formData.mdhz || "", //
njId: formData.targetNjIds.join(","), // ID
njmcId: formData.targetNjmcIds.join(","), // ID
bjId: formData.targetBjIds.join(","), // ID
@ -838,13 +878,13 @@ const saveDraft = async () => {
const jlDto = buildJlDto("B"); // B
//
const response = await jlSaveApi(jlDto);
const response = await jlSaveApi(jlDto) as ApiResponse<any>;
uni.hideLoading();
if (response && response.resultCode === 1) {
// ID
const jlId = response.result || response.data || jlDto.id;
const jlId = response.result || (response as any).data || jlDto.id;
// ID
if (jlId) {
@ -863,15 +903,12 @@ const saveDraft = async () => {
}, 2000);
} else {
uni.showToast({
title: response?.resultMsg || "保存草稿失败",
title: (response as any)?.resultMsg || "保存草稿失败",
icon: "error"
});
}
} catch (error) {
uni.hideLoading();
console.error("保存草稿失败:", error);
//
if (error instanceof Error) {
uni.showToast({
title: error.message,
@ -886,11 +923,6 @@ const saveDraft = async () => {
}
};
const previewNotice = () => {
if (!validateForm()) return;
uni.showToast({ title: "预览功能待实现", icon: "none" });
};
const publishNotice = async () => {
if (!validateForm()) return;
@ -909,7 +941,7 @@ const publishNotice = async () => {
// ID
if (formData.id) {
try {
const response = await jlFindByIdApi({ id: formData.id });
const response = await jlFindByIdApi({ id: formData.id }) as ApiResponse<any>;
if (response && response.resultCode === 1 && response.result) {
const jlData = response.result;
if (jlData.jlStatus === 'A') {
@ -917,9 +949,7 @@ const publishNotice = async () => {
return;
}
}
} catch (error) {
console.error("检查接龙状态失败:", error);
}
} catch (error) {}
}
try {
@ -931,20 +961,19 @@ const publishNotice = async () => {
//
const response = await Promise.race([
jlSaveApi(jlDto),
jlSaveApi(jlDto) as Promise<ApiResponse<any>>,
new Promise((_, reject) =>
setTimeout(() => reject(new Error("请求超时")), 300000) // 5
)
]);
]) as ApiResponse<any>;
uni.hideLoading();
if (response && response.resultCode === 1) {
// ID使ID使ID
const jlId = response.result || response.data || jlDto.id;
const jlId = response.result || (response as any).data || jlDto.id;
if (!jlId) {
console.error("发布成功但未获取到接龙ID");
uni.showToast({
title: "发布成功但获取接龙ID失败",
icon: "error"
@ -971,16 +1000,16 @@ const publishNotice = async () => {
});
}, 2000);
} else {
//
const errorMsg = (response as any)?.resultMsg || (response as any)?.message || "发布失败";
uni.showToast({
title: response?.resultMsg || "发布失败",
icon: "error"
title: errorMsg,
icon: "none",
duration: 3000
});
}
} catch (error) {
uni.hideLoading();
console.error("发布接龙失败:", error);
//
if (error instanceof Error) {
if (error.message === "请求超时") {
uni.showToast({
@ -996,7 +1025,7 @@ const publishNotice = async () => {
}
} else if (error && typeof error === 'object' && 'errMsg' in error) {
// uni-app
if (error.errMsg && error.errMsg.includes('timeout')) {
if ((error as any).errMsg && (error as any).errMsg.includes('timeout')) {
uni.showToast({
title: "请求超时,请检查网络连接或稍后重试",
icon: "none",
@ -1004,7 +1033,7 @@ const publishNotice = async () => {
});
} else {
uni.showToast({
title: error.errMsg || "发布失败,请重试",
title: (error as any).errMsg || "发布失败,请重试",
icon: "error"
});
}
@ -1018,6 +1047,15 @@ const publishNotice = async () => {
isPublishing.value = false;
}
};
//
watch(() => formData.title, (newTitle) => {
if (formData.signatureRequired && newTitle) {
const title = newTitle.trim() || "通知";
formData.mdhz = `我已认真阅读《${title}》内容,加强对孩子安全教育和安全监管,对孩子的安全全面负责。`;
}
}, { immediate: false });
</script>
<style scoped lang="scss">
@ -1154,6 +1192,14 @@ const publishNotice = async () => {
min-height: 80px;
}
.receipt-input ::v-deep .uni-easyinput__content-textarea {
font-size: 14px !important;
color: #606266 !important;
padding: 0 !important;
line-height: 1.5;
min-height: 60px;
}
.attachments-section {
padding-top: 15px;
.form-label {

View File

@ -0,0 +1,489 @@
<!-- src/pages/view/routine/ShiTangXunCha/add.vue -->
<template>
<view class="add-page">
<view class="form-container">
<view class="form-title">新增食堂巡查</view>
<!-- 工作名称 -->
<view class="form-item">
<text class="form-label">工作名称 *</text>
<input
v-model="formData.gzmc"
class="form-input"
placeholder="请输入工作名称"
maxlength="100"
/>
</view>
<!-- 工作描述 -->
<view class="form-item">
<text class="form-label">工作描述</text>
<textarea
v-model="formData.gzms"
class="form-textarea"
placeholder="请输入工作描述"
maxlength="500"
:show-count="true"
/>
</view>
<!-- 上传照片 -->
<view class="form-item">
<text class="form-label">上传照片</text>
<view class="upload-section">
<view class="upload-list">
<view
v-for="(image, index) in imageList"
:key="index"
class="upload-item"
>
<image
:src="image.url ? imagUrl(image.url) : image.tempPath"
mode="aspectFill"
class="upload-image"
@click="previewImage(index)"
/>
<view class="upload-delete" @click="deleteImage(index)">
<text class="delete-icon">×</text>
</view>
</view>
<view
v-if="imageList.length < 9"
class="upload-add"
@click="chooseImage"
>
<text class="add-icon">+</text>
<text class="add-text">添加图片</text>
</view>
</view>
</view>
</view>
<!-- 提交人 -->
<view class="form-item">
<text class="form-label">提交人</text>
<input
:value="formData.jsxm"
class="form-input"
disabled
placeholder="自动获取当前用户"
/>
</view>
<!-- 提交时间 -->
<view class="form-item">
<text class="form-label">提交时间</text>
<picker
mode="date"
:value="formData.tjtime"
@change="onTimeChange"
class="form-picker"
>
<view class="picker-display">
{{ formData.tjtime || '请选择提交时间' }}
</view>
</picker>
</view>
</view>
<!-- 提交按钮 -->
<view class="submit-section">
<u-button
text="提交"
type="primary"
:loading="submitting"
@click="handleSubmit"
class="submit-btn"
/>
</view>
</view>
</template>
<script lang="ts" setup>
import { hcSaveApi } from "@/api/base/hcApi";
import { attachmentUpload } from "@/api/system/upload";
import { useUserStore } from "@/store/modules/user";
import { showLoading, hideLoading, showToast } from "@/utils/uniapp";
import { imagUrl } from "@/utils";
import { ref, onMounted } from "vue";
const { getJs } = useUserStore();
//
const formData = ref({
gzmc: '', //
gzms: '', //
jsId: '', // ID
jsxm: '', //
tjtime: '', //
tjfj: '' //
});
// -
interface ImageItem {
tempPath?: string; //
url?: string; //
name?: string; //
}
const imageList = ref<ImageItem[]>([]);
//
const submitting = ref(false);
//
onMounted(() => {
const js = getJs;
formData.value.jsId = js.id;
formData.value.jsxm = js.jsxm;
//
const today = new Date();
formData.value.tjtime = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
});
//
const chooseImage = () => {
uni.chooseImage({
count: 9 - imageList.value.length,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: async (res) => {
//
const newImages = res.tempFilePaths.map(path => ({
tempPath: path,
name: path.split('/').pop() || 'image.jpg'
}));
imageList.value = [...imageList.value, ...newImages];
//
await uploadImages(newImages);
}
});
};
//
const uploadImages = async (images: ImageItem[]) => {
try {
showLoading('上传图片中...');
for (let i = 0; i < images.length; i++) {
const image = images[i];
if (image.tempPath) {
try {
//
const uploadResult: any = await attachmentUpload(image.tempPath as any);
if (uploadResult && uploadResult.resultCode === 1 && uploadResult.result && uploadResult.result.length > 0) {
//
const serverPath = uploadResult.result[0].filePath;
//
const index = imageList.value.findIndex(img => img.tempPath === image.tempPath);
if (index !== -1) {
imageList.value[index].url = serverPath;
//
delete imageList.value[index].tempPath;
}
} else {
throw new Error('上传响应格式异常');
}
} catch (error) {
console.error('图片上传失败:', error);
showToast(`${image.name || '图片'}上传失败`, 'none');
//
const index = imageList.value.findIndex(img => img.tempPath === image.tempPath);
if (index !== -1) {
imageList.value.splice(index, 1);
}
}
}
}
hideLoading();
showToast('图片上传完成', 'success');
} catch (error) {
hideLoading();
console.error('批量上传图片失败:', error);
showToast('图片上传失败,请重试', 'none');
}
};
//
const previewImage = (index: number) => {
const urls = imageList.value.map(img =>
img.url ? imagUrl(img.url) : img.tempPath
).filter(url => url);
uni.previewImage({
urls: urls,
current: index
});
};
//
const deleteImage = (index: number) => {
imageList.value.splice(index, 1);
};
//
const onTimeChange = (e: any) => {
formData.value.tjtime = e.detail.value;
};
//
const handleSubmit = async () => {
//
if (!formData.value.gzmc.trim()) {
showToast('请输入工作名称', 'none');
return;
}
if (!formData.value.tjtime) {
showToast('请选择提交时间', 'none');
return;
}
//
const hasUploadingImages = imageList.value.some(img => img.tempPath && !img.url);
if (hasUploadingImages) {
showToast('请等待图片上传完成', 'none');
return;
}
submitting.value = true;
showLoading('提交中...');
try {
//
let tjfj = '';
const uploadedImages = imageList.value.filter(img => img.url);
if (uploadedImages.length > 0) {
// 使
tjfj = uploadedImages.map(img => img.url).join(',');
}
const submitData = {
...formData.value,
tjfj
};
await hcSaveApi(submitData);
hideLoading();
showToast('提交成功', 'success');
//
setTimeout(() => {
uni.navigateBack();
}, 1500);
} catch (error) {
hideLoading();
showToast('提交失败,请重试', 'none');
console.error('提交失败:', error);
} finally {
submitting.value = false;
}
};
</script>
<style scoped lang="scss">
.add-page {
min-height: 100vh;
background-color: #f5f7fa;
padding: 16px;
box-sizing: border-box;
}
.form-container {
background-color: #ffffff;
border-radius: 12px;
padding: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
margin-bottom: 20px;
}
.form-title {
font-size: 18px;
font-weight: 600;
color: #2c3e50;
text-align: center;
margin-bottom: 24px;
padding-bottom: 12px;
border-bottom: 1px solid #f0f0f0;
}
.form-item {
margin-bottom: 20px;
.form-label {
display: block;
font-size: 14px;
color: #2c3e50;
font-weight: 500;
margin-bottom: 8px;
}
.form-input {
width: 100%;
height: 44px;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 0 12px;
font-size: 14px;
color: #495057;
background-color: #ffffff;
box-sizing: border-box;
&:focus {
border-color: #28a745;
outline: none;
}
&:disabled {
background-color: #f8f9fa;
color: #6c757d;
}
}
.form-textarea {
width: 100%;
min-height: 100px;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 12px;
font-size: 14px;
color: #495057;
background-color: #ffffff;
box-sizing: border-box;
resize: none;
&:focus {
border-color: #28a745;
outline: none;
}
}
.form-picker {
.picker-display {
width: 100%;
height: 44px;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 0 12px;
font-size: 14px;
color: #495057;
background-color: #ffffff;
display: flex;
align-items: center;
box-sizing: border-box;
}
}
}
.upload-section {
.upload-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.upload-item {
position: relative;
width: 80px;
height: 80px;
border-radius: 8px;
overflow: hidden;
border: 1px solid #e9ecef;
.upload-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.upload-delete {
position: absolute;
top: 4px;
right: 4px;
width: 20px;
height: 20px;
background-color: rgba(0, 0, 0, 0.6);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
.delete-icon {
color: #ffffff;
font-size: 14px;
font-weight: bold;
}
}
}
.upload-add {
width: 80px;
height: 80px;
border: 2px dashed #e9ecef;
border-radius: 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #f8f9fa;
cursor: pointer;
.add-icon {
font-size: 24px;
color: #6c757d;
margin-bottom: 4px;
}
.add-text {
font-size: 12px;
color: #6c757d;
}
}
}
.submit-section {
padding: 0 16px;
.submit-btn {
width: 100%;
height: 48px;
background: linear-gradient(135deg, #007aff 0%, #0056cc 100%);
border: none;
border-radius: 12px;
font-size: 16px;
font-weight: 600;
color: #ffffff;
box-shadow: 0 4px 16px rgba(0, 122, 255, 0.3);
transition: all 0.3s ease;
&:active {
transform: translateY(1px);
box-shadow: 0 2px 8px rgba(0, 122, 255, 0.4);
}
}
}
//
@media (max-width: 375px) {
.add-page {
padding: 12px;
}
.form-container {
padding: 16px;
}
.form-title {
font-size: 16px;
}
}
</style>

View File

@ -0,0 +1,284 @@
<!-- src/pages/view/routine/ShiTangXunCha/detail.vue -->
<template>
<view class="detail-page">
<view class="detail-container">
<view class="detail-header">
<text class="detail-title">{{ detailData.gzmc }}</text>
</view>
<!-- 工作描述 -->
<view class="detail-section">
<text class="section-title">工作描述</text>
<view class="section-content">
<rich-text class="detail-text" :nodes="detailData.gzms || '暂无描述'"></rich-text>
</view>
</view>
<!-- 照片展示 -->
<view v-if="imageList.length > 0" class="detail-section">
<text class="section-title">巡查照片</text>
<view class="image-grid">
<view
v-for="(image, index) in imageList"
:key="index"
class="image-item"
@click="previewImage(index)"
>
<image
:src="image"
mode="aspectFill"
class="detail-image"
/>
</view>
</view>
</view>
<!-- 提交信息 -->
<view class="detail-section">
<text class="section-title">提交信息</text>
<view class="info-list">
<view class="info-item">
<text class="info-label">提交人</text>
<text class="info-value">{{ detailData.jsxm || '未知' }}</text>
</view>
<view class="info-item">
<text class="info-label">提交时间</text>
<text class="info-value">{{ formatTime(detailData.tjtime) }}</text>
</view>
</view>
</view>
</view>
<!-- 操作按钮 -->
<view class="action-section">
<u-button
text="返回列表"
type="primary"
@click="goBack"
class="back-btn"
/>
</view>
</view>
</template>
<script lang="ts" setup>
import { hcFindByIdApi } from "@/api/base/hcApi";
import { imagUrl } from "@/utils";
import { ref, onMounted } from "vue";
import { onLoad } from "@dcloudio/uni-app";
interface DetailData {
id: string;
gzmc: string;
gzms: string;
jsId: string;
jsxm: string;
tjtime: string;
tjfj: string;
}
//
const detailData = ref<DetailData>({
id: '',
gzmc: '',
gzms: '',
jsId: '',
jsxm: '',
tjtime: '',
tjfj: ''
});
//
const imageList = ref<string[]>([]);
//
onLoad((options) => {
if (options.id) {
loadDetail(options.id);
}
});
//
const loadDetail = async (id: string) => {
try {
const res = await hcFindByIdApi({ id });
if (res && res.result) {
detailData.value = res.result;
//
if (detailData.value.tjfj) {
const images = detailData.value.tjfj.split(',').filter(img => img.trim());
imageList.value = images.map(img => imagUrl(img));
}
}
} catch (error) {
console.error('加载详情失败:', error);
uni.showToast({
title: '加载失败',
icon: 'none'
});
}
};
//
const previewImage = (index: number) => {
uni.previewImage({
urls: imageList.value,
current: index
});
};
//
const formatTime = (timeStr: string) => {
if (!timeStr) return "";
const date = new Date(timeStr);
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
};
//
const goBack = () => {
uni.navigateBack();
};
</script>
<style scoped lang="scss">
.detail-page {
min-height: 100vh;
background-color: #f5f7fa;
padding: 16px;
box-sizing: border-box;
}
.detail-container {
background-color: #ffffff;
border-radius: 12px;
padding: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
margin-bottom: 20px;
}
.detail-header {
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #f0f0f0;
.detail-title {
display: block;
font-size: 18px;
font-weight: 600;
color: #2c3e50;
margin-bottom: 8px;
line-height: 1.4;
}
.detail-time {
font-size: 14px;
color: #6c757d;
}
}
.detail-section {
margin-bottom: 24px;
.section-title {
display: block;
font-size: 16px;
font-weight: 600;
color: #2c3e50;
margin-bottom: 12px;
padding-left: 8px;
border-left: 3px solid #28a745;
}
.section-content {
.detail-text {
font-size: 14px;
color: #5a6c7d;
line-height: 1.6;
word-break: break-word;
}
}
}
.image-grid {
display: flex;
flex-wrap: wrap;
gap: 8px;
.image-item {
width: calc(33.33% - 6px);
aspect-ratio: 1;
border-radius: 8px;
overflow: hidden;
border: 1px solid #e9ecef;
.detail-image {
width: 100%;
height: 100%;
object-fit: cover;
}
}
}
.info-list {
.info-item {
display: flex;
align-items: center;
margin-bottom: 12px;
.info-label {
font-size: 14px;
color: #6c757d;
min-width: 80px;
flex-shrink: 0;
}
.info-value {
font-size: 14px;
color: #2c3e50;
font-weight: 500;
}
}
}
.action-section {
padding: 0 16px;
.back-btn {
width: 100%;
height: 48px;
background: linear-gradient(135deg, #007aff 0%, #0056cc 100%);
border: none;
border-radius: 12px;
font-size: 16px;
font-weight: 600;
color: #ffffff;
box-shadow: 0 4px 16px rgba(0, 122, 255, 0.3);
transition: all 0.3s ease;
&:active {
transform: translateY(1px);
box-shadow: 0 2px 8px rgba(0, 122, 255, 0.4);
}
}
}
//
@media (max-width: 375px) {
.detail-page {
padding: 12px;
}
.detail-container {
padding: 16px;
}
.detail-header .detail-title {
font-size: 16px;
}
.image-grid .image-item {
width: calc(50% - 4px);
}
}
</style>

View File

@ -0,0 +1,477 @@
<!-- src/pages/view/routine/ShiTangXunCha/index.vue -->
<template>
<view class="hc-list-page">
<!-- 查询组件 -->
<view class="query-component">
<view class="search-card">
<view class="search-item">
<picker
mode="date"
:value="searchForm.startTime"
@change="onStartTimeChange"
class="date-picker"
>
<view class="picker-text">{{ searchForm.startTime || '开始时间' }}</view>
</picker>
<text class="date-separator">~</text>
<picker
mode="date"
:value="searchForm.endTime"
@change="onEndTimeChange"
class="date-picker"
>
<view class="picker-text">{{ searchForm.endTime || '结束时间' }}</view>
</picker>
</view>
<view class="search-actions">
<u-button
text="查询"
type="primary"
size="small"
@click="handleSearch"
class="search-btn"
/>
<u-button
text="重置"
type="info"
size="small"
@click="handleReset"
class="reset-btn"
/>
</view>
</view>
</view>
<!-- 列表组件 -->
<view class="list-component">
<scroll-view scroll-y class="list-scroll-view">
<view v-if="isLoading" class="loading-indicator">加载中...</view>
<template v-else-if="dataList.length > 0">
<view v-for="data in dataList" :key="data.id" class="hc-card" @click="goToDetail(data.id)">
<view class="card-header">
<text class="hc-title">{{ data.gzmc }}</text>
<text class="hc-time">{{ formatTime(data.tjtime) }}</text>
</view>
<view class="card-body">
<rich-text class="hc-excerpt" :nodes="data.gzms"></rich-text>
<view v-if="data.tjfj" class="image-preview">
<image
:src="getImageUrl(data.tjfj)"
mode="aspectFill"
class="preview-image"
></image>
</view>
</view>
<view class="card-footer">
<text class="footer-item">提交人: {{ data.jsxm || '未知' }}</text>
<text class="footer-item">提交时间: {{ formatTime(data.tjtime) }}</text>
</view>
</view>
</template>
<view v-else class="empty-state">暂无巡查数据</view>
</scroll-view>
</view>
<!-- 新增按钮 - 固定在底部 -->
<view class="add-button-fixed">
<u-button
text="新增巡查"
type="primary"
@click="goToAdd"
class="add-btn"
/>
</view>
</view>
</template>
<script lang="ts" setup>
import { hcFindPageApi } from "@/api/base/hcApi";
import { imagUrl } from "@/utils";
import { ref, onMounted } from "vue";
import { onShow } from "@dcloudio/uni-app";
interface HcItem {
id: string;
gzmc: string; //
gzms: string; //
jsId: string; // ID
jsxm: string; //
tjtime: string; //
tjfj: string; //
}
//
const getCurrentMonthStart = () => {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
return `${year}-${month}-01`;
};
//
const getCurrentMonthEnd = () => {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const lastDay = new Date(year, now.getMonth() + 1, 0).getDate();
return `${year}-${month}-${lastDay}`;
};
//
const searchForm = ref({
startTime: getCurrentMonthStart(),
endTime: getCurrentMonthEnd()
});
//
const dataList = ref<HcItem[]>([]);
const isLoading = ref(false);
//
const onStartTimeChange = (e: any) => {
searchForm.value.startTime = e.detail.value;
};
//
const onEndTimeChange = (e: any) => {
searchForm.value.endTime = e.detail.value;
};
//
const handleSearch = () => {
getHcList();
};
//
const handleReset = () => {
searchForm.value.startTime = getCurrentMonthStart();
searchForm.value.endTime = getCurrentMonthEnd();
getHcList();
};
//
const getHcList = async () => {
isLoading.value = true;
try {
const res = await hcFindPageApi({
startTime: searchForm.value.startTime,
endTime: searchForm.value.endTime,
page: 1,
rows: 100
});
dataList.value = res.rows || [];
} catch (error) {
console.error('获取巡查列表失败:', error);
dataList.value = [];
} finally {
isLoading.value = false;
}
};
//
const goToDetail = (hcId: string) => {
uni.navigateTo({
url: `/pages/view/routine/ShiTangXunCha/detail?id=${hcId}`,
});
};
//
const goToAdd = () => {
uni.navigateTo({
url: "/pages/view/routine/ShiTangXunCha/add",
});
};
//
const formatTime = (timeStr: string) => {
if (!timeStr) return "";
const date = new Date(timeStr);
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(
2,
"0"
)}-${String(date.getDate()).padStart(2, "0")}`;
};
// URL
const getImageUrl = (imagePath: string) => {
if (!imagePath) return "";
//
const firstImage = imagePath.split(',')[0];
return imagUrl(firstImage);
};
onShow(() => {
getHcList();
});
//
onMounted(() => {
getHcList();
});
</script>
<style scoped lang="scss">
.hc-list-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f7fa;
}
//
.query-component {
padding: 12px;
background-color: #fff;
border-bottom: 1px solid #eee;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
flex-shrink: 0;
}
.search-card {
background-color: #ffffff;
border-radius: 12px;
padding: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
border: 1px solid #f0f0f0;
}
.search-item {
display: flex;
align-items: center;
margin-bottom: 12px;
gap: 8px;
.date-picker {
flex: 1;
min-width: 120px;
}
.picker-text {
background-color: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 8px 12px;
font-size: 14px;
color: #495057;
text-align: center;
}
.date-separator {
font-size: 16px;
color: #6c757d;
margin: 0 8px;
font-weight: bold;
}
}
.search-actions {
display: flex;
gap: 12px;
justify-content: center;
.search-btn, .reset-btn {
flex: 1;
max-width: 120px;
}
}
//
.list-component {
flex: 1;
overflow-y: auto;
padding: 12px;
padding-bottom: 80px; //
box-sizing: border-box;
background-color: #f5f7fa;
}
.list-scroll-view {
height: 100%;
}
.loading-indicator,
.empty-state {
text-align: center;
color: #999;
padding: 30px 15px;
font-size: 14px;
}
.hc-card {
background-color: #ffffff;
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
border: 1px solid #f0f0f0;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
&:active {
transform: translateY(1px);
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.12);
}
&::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 4px;
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
border-radius: 2px;
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
gap: 12px;
.hc-title {
font-size: 16px;
font-weight: 600;
color: #2c3e50;
flex: 1;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
word-break: break-word;
}
.hc-time {
font-size: 12px;
color: #6c757d;
white-space: nowrap;
flex-shrink: 0;
}
}
.card-body {
margin-bottom: 12px;
.hc-excerpt {
font-size: 14px;
color: #5a6c7d;
line-height: 1.6;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
word-break: break-word;
margin-bottom: 8px;
}
.image-preview {
.preview-image {
width: 80px;
height: 60px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
object-fit: cover;
}
}
}
.card-footer {
display: flex;
flex-wrap: wrap;
gap: 8px 16px;
align-items: center;
padding-top: 12px;
border-top: 1px solid #f0f0f0;
.footer-item {
white-space: nowrap;
font-size: 13px;
color: #7f8c8d;
overflow: hidden;
text-overflow: ellipsis;
max-width: 180px;
display: flex;
align-items: center;
&::before {
content: '';
width: 4px;
height: 4px;
background-color: #bdc3c7;
border-radius: 50%;
margin-right: 6px;
flex-shrink: 0;
}
}
}
// -
.add-button-fixed {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 15px;
background-color: #fff;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
z-index: 10;
border-top: 1px solid #eee;
.add-btn {
width: 100%;
background: linear-gradient(135deg, #007aff 0%, #0056cc 100%);
border-radius: 12px;
padding: 12px 24px;
box-shadow: 0 4px 16px rgba(0, 122, 255, 0.3);
font-weight: 600;
font-size: 16px;
&:active {
transform: translateY(1px);
}
}
}
//
@media (max-width: 375px) {
.query-component {
padding: 8px;
}
.hc-card {
padding: 12px;
margin-bottom: 8px;
}
.card-header .hc-title {
font-size: 15px;
}
.card-footer .footer-item {
max-width: 150px;
font-size: 12px;
}
}
//
.hc-card {
animation: fadeInUp 0.3s ease-out;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB