zhxy-jsd/src/pages/view/notice/publish.vue

894 lines
22 KiB
Vue
Raw Normal View History

2025-04-22 10:22:33 +08:00
<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>