zhxy-jsd/src/pages/view/notice/publish.vue
2025-04-22 10:22:33 +08:00

894 lines
22 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<BasicLayout>
<!-- Default slot -->
<scroll-view scroll-y class="form-scroll-view">
<view class="form-container">
<view class="info-card main-content-card">
<view class="form-item cover-section">
<view class="cover-header">
<text class="form-label">封面</text>
<text class="cover-hint">建议尺寸xxx</text>
<text class="cover-counter"
>{{ formData.coverImage ? 1 : 0 }}/1</text
>
</view>
<view class="cover-upload-wrapper">
<CustomUpload
field="coverImage"
:value="formData.coverImage"
@select="handleCoverSelected"
@close="handleCoverClosed"
>
<view class="cover-placeholder">
<uni-icons type="image" size="40" color="#b0b0b0"></uni-icons>
<text>添加封面</text>
</view>
</CustomUpload>
</view>
</view>
<view class="form-item">
<uni-easyinput
type="textarea"
autoHeight
v-model="formData.title"
placeholder="请输入通知标题 (必填)"
:inputBorder="false"
placeholder-style="font-weight:bold; font-size: 18px; color: #999;"
class="title-input"
></uni-easyinput>
</view>
<view class="form-item">
<uni-easyinput
type="textarea"
autoHeight
v-model="formData.content"
placeholder="请输入通知内容 (必填)"
:inputBorder="false"
class="content-input"
></uni-easyinput>
</view>
<view class="form-item attachments-section">
<text class="form-label">附件</text>
<view class="attachment-list">
<view
v-for="(att, index) in formData.attachments"
:key="index"
class="attachment-item"
>
<uni-icons
:type="getAttachmentIcon(att.type)"
size="20"
color="#666"
class="attachment-icon"
></uni-icons>
<text class="attachment-name" @click="previewAttachment(att)">{{
att.name
}}</text>
<uni-icons
type="closeempty"
size="18"
color="#999"
class="remove-icon"
@click="removeAttachment(index)"
></uni-icons>
</view>
</view>
<view class="add-attachment-placeholder" @click="addAttachment">
<view class="add-icon"
><uni-icons type="plusempty" size="20" color="#ccc"></uni-icons
></view>
<text class="placeholder-text"
>添加图文/视频/文件/公众号/小程序等</text
>
</view>
</view>
</view>
<view class="info-card">
<picker
mode="selector"
:range="combinedClassRange"
:value="selectedClassIndex"
@change="onClassPickerChange"
>
<view class="card-header picker-header">
<text class="section-title">按名单填写</text>
<view class="target-class">
<text :class="{ placeholder: !formData.targetClass }">{{
formData.targetClass || "请选择班级"
}}</text>
<uni-icons type="right" size="16" color="#999"></uni-icons>
</view>
</view>
</picker>
<view class="name-tags">
<text
v-for="name in formData.targetNames"
:key="name"
class="name-tag"
>{{ name }}</text
>
<button
v-if="formData.targetNames.length > 0"
size="mini"
type="default"
class="modify-btn"
@click="handleModifyNames"
>
修改
</button>
</view>
</view>
<view class="info-card list-item-card">
<picker
mode="selector"
:range="signatureOptions"
@change="handleSignatureChange"
>
<view class="list-item-row">
<text class="list-label">按名单签字</text>
<view class="list-value">
<text>{{ signatureStatusText }}</text>
<uni-icons type="right" size="16" color="#999"></uni-icons>
</view>
</view>
</picker>
</view>
<view class="info-card list-item-card">
<uni-datetime-picker type="datetime" v-model="formData.startTime">
<view class="list-item-row">
<text class="list-label">开始时间</text>
<view class="list-value">
<text>{{ formData.startTime || "请选择" }}</text>
<uni-icons type="right" size="16" color="#999"></uni-icons>
</view>
</view>
</uni-datetime-picker>
<uni-datetime-picker type="datetime" v-model="formData.endTime">
<view class="list-item-row no-border">
<text class="list-label">结束时间</text>
<view class="list-value">
<text>{{ formData.endTime || "请选择" }}</text>
<uni-icons type="right" size="16" color="#999"></uni-icons>
</view>
</view>
</uni-datetime-picker>
</view>
</view>
</scroll-view>
<!-- Bottom slot -->
<template #bottom>
<view class="bottom-actions">
<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">
立即发布
</button>
</view>
</template>
</BasicLayout>
</template>
<script lang="ts" setup>
import { ref, reactive, computed } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import CustomUpload from "/src/components/BasicUpload/CustomUpload.vue";
interface Attachment {
name: string;
type: string;
url: string;
size?: number;
}
interface FormData {
id?: string;
title: string;
content: string;
coverImage: string | null;
attachments: Attachment[];
targetClass: string;
targetNames: string[];
targetStudentIds: string[];
signatureRequired: boolean;
startTime: string;
endTime: string;
}
const noticeId = ref<string | null>(null);
const isLoadingStudents = ref(false);
const formData = reactive<FormData>({
title: "",
content: "",
coverImage: null,
attachments: [],
targetClass: "",
targetNames: [],
targetStudentIds: [],
signatureRequired: false,
startTime: "",
endTime: "",
});
const signatureOptions = ["不启用", "启用"];
const signatureStatusText = computed(() => {
return formData.signatureRequired ? "启用" : "不启用";
});
const classList = ref([
{ id: "g1c1", name: "一年级1班" },
{ id: "g1c2", name: "一年级2班" },
{ id: "g1c3", name: "一年级3班" },
{ id: "g1c4", name: "一年级4班" },
{ id: "g1c5", name: "一年级5班" },
{ id: "g2c1", name: "二年级1班" },
{ id: "g2c2", name: "二年级2班" },
{ id: "g2c3", name: "二年级3班" },
{ id: "g3c1", name: "三年级1班" },
{ id: "g3c2", name: "三年级2班" },
{ id: "g3c3", name: "三年级3班" },
{ id: "g3c4", name: "三年级4班" },
{ id: "g4c1", name: "四年级1班" },
{ id: "g4c2", name: "四年级2班" },
{ id: "g5c1", name: "五年级1班" },
{ id: "g5c2", name: "五年级2班" },
{ id: "g5c3", name: "五年级3班" },
{ id: "g6c1", name: "六年级1班" },
{ id: "g6c2", name: "六年级2班" },
]);
const combinedClassRange = computed(() => {
return classList.value.map((cls) => cls.name);
});
const selectedClassIndex = computed(() => {
const index = classList.value.findIndex(
(cls) => cls.name === formData.targetClass
);
return index >= 0 ? index : 0;
});
const fetchStudentsByClass = async (className: string): Promise<string[]> => {
console.log(`模拟获取班级 [${className}] 的学生列表...`);
isLoadingStudents.value = true;
await new Promise((resolve) => setTimeout(resolve, 400));
const mockStudents = [
"施延兴",
"安苒溪",
"罗浩晨",
"康萌",
"范文昊",
"丁贺祥",
"韦运昊",
"萧润丽",
"谢林",
"鲍泽远",
"杨俊",
];
isLoadingStudents.value = false;
return mockStudents;
};
onLoad((options) => {
if (options && options.id) {
noticeId.value = options.id;
uni.setNavigationBarTitle({ title: "编辑通知" });
formData.title = "关于五一放假的通知 (编辑)";
formData.content = "根据校历安排现将2024年五一劳动节放假安排通知如下...";
formData.targetClass = "二年级1班";
formData.targetNames = ["张三", "李四"];
formData.targetStudentIds = ["s201-mock", "s202-mock"];
formData.signatureRequired = true;
formData.startTime = "2024-04-30 18:00:00";
formData.endTime = "2024-05-05 23:59:59";
if (formData.targetClass && noticeId.value) {
fetchStudentsByClass(formData.targetClass).then((students) => {
formData.targetNames = students;
});
}
} else {
uni.setNavigationBarTitle({ title: "发布通知" });
}
});
const handleCoverSelected = (e: any) => {
console.log("选择封面 (CustomUpload):", e);
if (e.tempFilePaths && e.tempFilePaths.length > 0) {
formData.coverImage = e.tempFilePaths[0];
console.log("封面临时路径:", formData.coverImage);
} else {
console.error("无法从选择事件中获取封面路径:", e);
}
};
const handleCoverClosed = (field: string) => {
console.log(`删除封面 (CustomUpload): field=${field}`);
if (field === "coverImage") {
formData.coverImage = null;
}
};
const addAttachment = () => {
uni.chooseFile({
count: 5,
type: "all",
success: (res) => {
const tempFiles = res.tempFiles;
if (Array.isArray(tempFiles) && tempFiles.length > 0) {
tempFiles.forEach((file: any) => {
let fileType = "file";
const fileName = file.name || "";
const fileExtension = fileName.split(".").pop()?.toLowerCase();
if (
["png", "jpg", "jpeg", "gif", "bmp", "webp"].includes(
fileExtension || ""
)
) {
fileType = "image";
} else if (
["mp4", "mov", "avi", "wmv", "flv"].includes(fileExtension || "")
) {
fileType = "video";
} else if (
["mp3", "wav", "aac", "ogg"].includes(fileExtension || "")
) {
fileType = "audio";
}
if (
file.type &&
typeof file.type === "string" &&
(file.type.startsWith("image/") ||
file.type.startsWith("video/") ||
file.type.startsWith("audio/"))
) {
fileType = file.type.split("/")[0];
}
formData.attachments.push({
name: fileName,
type: fileType,
url: file.path,
size: file.size,
});
});
} else {
console.log("未选择任何文件或返回结果异常,或 tempFiles 不是数组");
}
},
fail: (err) => {
console.error("选择附件失败:", err);
if (err.errMsg && !err.errMsg.includes("cancel")) {
uni.showToast({ title: "选择附件失败", icon: "none" });
}
},
});
};
const removeAttachment = (index: number) => {
formData.attachments.splice(index, 1);
};
const getAttachmentIcon = (type: string): string => {
if (type === "image") return "image";
if (type === "video") return "videocam";
if (type === "audio") return "mic";
return "paperclip";
};
const previewAttachment = (attachment: Attachment) => {
console.log("预览附件:", attachment);
uni.showToast({ title: `预览 ${attachment.name} 功能待实现`, icon: "none" });
};
const onClassPickerChange = async (e: any) => {
const index = e.detail.value;
const selectedClass = classList.value[index];
if (selectedClass && selectedClass.name !== formData.targetClass) {
formData.targetClass = selectedClass.name;
formData.targetNames = [];
formData.targetStudentIds = [];
try {
const students = await fetchStudentsByClass(formData.targetClass);
formData.targetNames = students;
} catch (error) {
console.error("获取学生列表失败:", error);
uni.showToast({ title: "获取学生列表失败", icon: "none" });
}
}
};
const handleModifyNames = () => {
const selectedClassObj = classList.value.find(
(cls) => cls.name === formData.targetClass
);
if (!selectedClassObj) {
uni.showToast({ title: "请先选择班级", icon: "none" });
return;
}
const classId = selectedClassObj.id;
uni.navigateTo({
url: `/pages/view/notice/selectStudents?classId=${classId}`,
});
};
const handleSignatureChange = (e: any) => {
const index = e.detail.value;
formData.signatureRequired = index == 1;
};
const validateForm = (): boolean => {
if (!formData.title.trim()) {
uni.showToast({ title: "请输入通知标题", icon: "none" });
return false;
}
if (!formData.content.trim()) {
uni.showToast({ title: "请输入通知内容", icon: "none" });
return false;
}
if (
formData.startTime &&
formData.endTime &&
formData.startTime >= formData.endTime
) {
uni.showToast({ title: "结束时间必须晚于开始时间", icon: "none" });
return false;
}
return true;
};
const saveDraft = () => {
if (!validateForm()) return;
console.log("保存草稿", formData);
uni.showToast({ title: "草稿保存成功 (模拟)", icon: "success" });
};
const previewNotice = () => {
if (!validateForm()) return;
console.log("预览通知", formData);
uni.showToast({ title: "预览功能待实现", icon: "none" });
};
const publishNotice = () => {
if (!validateForm()) return;
console.log("发布通知", formData);
uni.showLoading({ title: "发布中..." });
setTimeout(() => {
uni.hideLoading();
uni.showToast({ title: "发布成功 (模拟)", icon: "success" });
uni.navigateBack();
}, 1000);
};
</script>
<style scoped lang="scss">
/* Remove original page container styles */
/*
.notice-publish-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f4f5f7;
}
*/
.form-scroll-view {
/* Let BasicLayout handle height/flex */
/* flex: 1; */
/* height: 0; */
/* BasicLayout default slot might have padding, adjust if needed */
/* Or keep the scroll view if BasicLayout doesn't provide one */
height: 100%; // Assume BasicLayout's default slot needs this
box-sizing: border-box;
}
.form-container {
padding: 12px;
box-sizing: border-box;
/* Add padding-bottom if content gets hidden by bottom bar */
/* padding-bottom: 70px; */
}
.info-card {
background-color: #ffffff;
border-radius: 8px;
padding: 15px;
margin-bottom: 12px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
}
.main-content-card {
padding: 5px 15px;
.form-item {
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
padding-bottom: 5px;
}
&:first-child {
padding-top: 10px;
}
}
.cover-section {
padding-bottom: 10px;
.cover-header {
display: flex;
align-items: baseline;
margin-bottom: 10px;
width: 100%;
}
.form-label {
width: auto;
margin-right: 8px;
color: #303133;
font-size: 15px;
font-weight: 500;
flex-shrink: 0;
}
.cover-hint {
font-size: 12px;
color: #909399;
margin-right: auto;
padding-left: 5px;
}
.cover-counter {
font-size: 14px;
color: #c0c4cc;
flex-shrink: 0;
}
.cover-upload-wrapper {
width: 100%;
height: 150px;
border-radius: 6px;
overflow: hidden;
border: 1px dashed #dcdcdc;
background-color: #f8f8f8;
}
.cover-placeholder {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
color: #b0b0b0;
font-size: 14px;
box-sizing: border-box;
cursor: pointer;
text {
margin-top: 8px;
}
.uni-icons {
margin-bottom: 4px;
}
}
}
.title-input {
padding-bottom: 5px;
}
.content-input {
padding-top: 5px;
}
.title-input ::v-deep .uni-easyinput__content-textarea {
font-size: 18px !important;
font-weight: bold !important;
color: #303133 !important;
padding: 0 !important;
line-height: 1.5;
}
.content-input ::v-deep .uni-easyinput__content-textarea {
font-size: 15px !important;
color: #606266 !important;
padding: 0 !important;
line-height: 1.6;
min-height: 80px;
}
.attachments-section {
padding-top: 15px;
.form-label {
display: block;
color: #333;
font-size: 15px;
margin-bottom: 12px;
}
.attachment-list {
margin-bottom: 12px;
}
.attachment-item {
display: flex;
align-items: center;
background-color: #f8f9fa;
border-radius: 6px;
padding: 8px 12px;
margin-bottom: 8px;
border: 1px solid #e9ecef;
.attachment-icon {
margin-right: 8px;
flex-shrink: 0;
}
.attachment-name {
flex-grow: 1;
font-size: 14px;
color: #495057;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-right: 10px;
cursor: pointer;
}
.remove-icon {
flex-shrink: 0;
cursor: pointer;
opacity: 0.7;
&:hover {
opacity: 1;
color: #dc3545 !important;
}
}
}
.add-attachment-placeholder {
display: flex;
align-items: center;
border: 1px dashed #d5d8de;
border-radius: 6px;
padding: 15px;
background-color: #f8f8f8;
cursor: pointer;
transition: background-color 0.2s;
.add-icon {
width: 30px;
height: 30px;
border: 1px solid #d5d8de;
border-radius: 4px;
display: flex;
justify-content: center;
align-items: center;
margin-right: 12px;
background-color: #fff;
.uni-icons {
color: #999 !important;
}
}
.placeholder-text {
font-size: 14px;
color: #909399;
}
&:active {
background-color: #eee;
}
}
}
}
.info-card picker {
width: 100%;
}
.picker-header {
margin-bottom: 0;
padding: 14px 0;
border-bottom: 1px solid #f0f0f0;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
.section-title {
font-size: 16px;
font-weight: 500;
color: #303133;
padding-left: 15px;
}
.target-class {
display: flex;
align-items: center;
font-size: 14px;
color: #606266;
padding-right: 15px;
text {
margin-right: 5px;
&.placeholder {
color: #909399;
}
}
.uni-icons {
color: #909399 !important;
}
}
}
.name-tags {
padding: 15px;
margin-top: 0;
display: flex;
flex-wrap: wrap;
gap: 8px 10px;
position: relative;
min-height: 30px;
.name-tag,
.modify-btn {
font-size: 13px;
padding: 5px 0;
border-radius: 4px;
text-align: center;
flex-grow: 0;
flex-shrink: 0;
box-sizing: border-box;
flex-basis: calc((100% - 50px) / 6);
height: 28px;
line-height: 18px;
}
.name-tag {
background-color: #f4f4f5;
color: #909399;
}
.modify-btn {
background-color: #ecf5ff;
color: #409eff;
border: 1px solid #d9ecff;
padding: 0;
margin-left: 0;
margin-top: 0;
margin-bottom: 0;
&::after {
border: none;
}
display: flex;
justify-content: center;
align-items: center;
}
.loading-spinner {
position: absolute;
top: 5px;
left: 15px;
font-size: 12px;
color: #999;
}
}
.list-item-card {
padding: 0;
.uni-datetime-picker,
.picker {
width: 100%;
}
}
.list-item-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 15px;
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
transition: background-color 0.2s;
&:active {
background-color: #fafafa;
}
&.no-border {
border-bottom: none;
}
.list-label {
font-size: 15px;
color: #303133;
}
.list-value {
display: flex;
align-items: center;
font-size: 15px;
color: #909399;
text {
margin-right: 5px;
color: #606266;
&.placeholder {
color: #909399;
}
}
.uni-icons {
color: #c0c4cc !important;
}
}
uni-datetime-picker .list-value text,
picker .list-value text {
color: #606266;
}
uni-datetime-picker .list-value text:empty::before,
picker .list-value text:empty::before {
content: "请选择";
color: #909399;
}
picker .list-value text {
color: #606266;
}
}
.bottom-actions {
display: flex;
justify-content: space-around;
align-items: center;
padding: 12px 15px;
background-color: #ffffff;
border-top: 1px solid #e5e5e5;
.action-btn {
width: auto;
min-width: 90px;
margin: 0 5px;
font-size: 15px;
height: 40px;
line-height: 40px;
border-radius: 20px;
padding: 0 20px;
&::after {
border: none;
}
&.draft-btn {
background-color: #f4f4f5;
color: #909399;
border: 1px solid #e9e9eb;
}
&.preview-btn {
background-color: #ecf5ff;
color: #409eff;
border: 1px solid #d9ecff;
}
&.publish-btn {
background-color: #409eff;
color: #ffffff;
border: none;
}
&.draft-btn:active {
background-color: #e0e0e0;
}
&.preview-btn:active {
background-color: #d9ecff;
}
&.publish-btn:active {
background-color: #3a8ee6;
}
}
}
.target-class text.placeholder {
color: #909399;
}
</style>