index.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744
  1. <template>
  2. <view class="page_publish">
  3. <view class="publish_header">
  4. <uni-icons type="back" size="24" color="#333333" @click="goBack"></uni-icons>
  5. <text class="header_title">发布通知</text>
  6. <text class="header_placeholder"></text>
  7. </view>
  8. <scroll-view scroll-y class="publish_content">
  9. <view class="form_item">
  10. <view class="form_row form_row_type">
  11. <text class="form_label">通知类型</text>
  12. <view class="form_content">
  13. <picker mode="selector" :range="typeOptions" :value="typeIndex" @change="handleTypeChange">
  14. <view class="filter_item">
  15. <text class="filter_text" :class="{ placeholder: typeIndex === -1 }">{{
  16. formData.noticeType || '请选择类型' }}</text>
  17. <uni-icons type="down" size="16" color="#999999"></uni-icons>
  18. </view>
  19. </picker>
  20. </view>
  21. </view>
  22. </view>
  23. <view class="form_item">
  24. <view class="form_row form_row_type">
  25. <text class="form_label">通知标题</text>
  26. <view class="form_content">
  27. <input type="text" class="form_input" v-model="formData.title" placeholder="请输入任务名称" />
  28. </view>
  29. </view>
  30. </view>
  31. <view class="form_item">
  32. <view class="form_row">
  33. <text class="form_label">通知内容</text>
  34. <view class="form_content">
  35. <view class="textarea_container">
  36. <textarea class="form_textarea" v-model="formData.content" placeholder="请输入通知内容"
  37. :maxlength="-1"></textarea>
  38. <!-- <view class="textarea_actions">
  39. <image src="/static/image/publish/full.png" class="full_icon" mode="aspectFit"></image>
  40. <text class="action_text">全屏编辑</text>
  41. </view> -->
  42. </view>
  43. </view>
  44. </view>
  45. </view>
  46. <view class="form_item">
  47. <view class="form_row">
  48. <text class="form_label">发布时间</text>
  49. <view class="form_content">
  50. <custom-radio v-model="publishType" :options="publishOptions" />
  51. <view v-if="publishType === 1" class="time_picker">
  52. <uni-datetime-picker v-model="publishDateTime" type="datetime" :start="startTime"
  53. :end="endTime" return-type="string" />
  54. </view>
  55. </view>
  56. </view>
  57. </view>
  58. <view class="form_item">
  59. <view class="form_row">
  60. <text class="form_label">通知对象</text>
  61. <view class="form_content">
  62. <view class="object_row">
  63. <view class="object_info" @click="goPersonList">
  64. <text class="object_count">{{ objectTypeLabel || '请选择' }}</text>
  65. <text class="object_count">已选{{ selectedCount }}人</text>
  66. </view>
  67. <view class="object_action" @click="selectObject">
  68. <text class="action_text">选择通知对象</text>
  69. <text class="action_arrow">></text>
  70. </view>
  71. </view>
  72. </view>
  73. </view>
  74. </view>
  75. <view class="form_item" v-if="isEdit">
  76. <view class="form_row">
  77. <text class="form_label">通知状态</text>
  78. <view class="form_content">
  79. <view class="switch_row">
  80. <view class="switch_container">
  81. <switch :checked="formData.status" color="#2E64FA" @change="onStatusChange" />
  82. </view>
  83. <text class="switch_text">说明:关闭后通知不对通知对象显示</text>
  84. </view>
  85. </view>
  86. </view>
  87. </view>
  88. <view class="form_item" v-if="isEdit && formData.status">
  89. <view class="form_row">
  90. <text class="form_label">已读记录</text>
  91. <view class="form_content">
  92. <custom-radio v-model="saveRecord" :options="recordOptions" />
  93. <text class="record_tip">说明:{{ saveRecord === 0 ? '已读记录将被保留' : '已读记录将被清空,重新记录' }}</text>
  94. </view>
  95. </view>
  96. </view>
  97. </scroll-view>
  98. <view class="publish_footer">
  99. <view class="confirm_btn" @click="handleSave">
  100. <text class="btn_text">确定</text>
  101. </view>
  102. </view>
  103. </view>
  104. </template>
  105. <script setup>
  106. import { ref, reactive, onMounted } from 'vue';
  107. import { onShow } from '@dcloudio/uni-app';
  108. import uniIcons from '@/uni_modules/uni-icons/components/uni-icons/uni-icons.vue';
  109. import uniDatetimePicker from '@/uni_modules/uni-datetime-picker/components/uni-datetime-picker/uni-datetime-picker.vue';
  110. import overviewApi from '@/reqApi/overview.js';
  111. import CustomRadio from '@/components/customRadio.vue';
  112. const showTypePicker = ref(false);
  113. const publishType = ref(0);
  114. const isEdit = ref(false);
  115. const saveRecord = ref(0);
  116. const typeIndex = ref(-1);
  117. const typeData = ref([]);
  118. const typeOptions = ref([]);
  119. const getCurrentDateTime = () => {
  120. const now = new Date();
  121. const year = now.getFullYear();
  122. const month = String(now.getMonth() + 1).padStart(2, '0');
  123. const day = String(now.getDate()).padStart(2, '0');
  124. const hours = String(now.getHours()).padStart(2, '0');
  125. const minutes = String(now.getMinutes()).padStart(2, '0');
  126. const seconds = String(now.getSeconds()).padStart(2, '0');
  127. return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
  128. };
  129. const publishDateTime = ref(getCurrentDateTime());
  130. const startTime = ref('2024-01-01 00:00:00');
  131. const endTime = ref('2030-12-31 23:59:59');
  132. const publishOptions = [
  133. { label: '立即发布', value: 0 },
  134. { label: '预约发布', value: 1 }
  135. ];
  136. const recordOptions = [
  137. { label: '保存', value: 0 },
  138. { label: '不保存', value: 1 }
  139. ];
  140. const formData = reactive({
  141. title: '',
  142. noticeType: '',
  143. content: '',
  144. status: true
  145. });
  146. const selectedCount = ref(0);
  147. const selectedUsers = ref([]);
  148. const schoolYearId = ref('');
  149. const noticeTypeId = ref('');
  150. const objectTypeLabel = ref('');
  151. const editId = ref('');
  152. const originalNoticeType = ref(0);
  153. const fetchNoticeTypeList = async () => {
  154. try {
  155. const res = await overviewApi.noticeTypeList();
  156. if (res.data && Array.isArray(res.data)) {
  157. typeData.value = res.data.map(item => ({
  158. id: item.id,
  159. name: item.name
  160. }));
  161. typeOptions.value = typeData.value.map(item => item.name);
  162. }
  163. } catch (error) {
  164. console.error('获取通知类型失败:', error);
  165. }
  166. };
  167. onMounted(() => {
  168. fetchNoticeTypeList();
  169. const pages = getCurrentPages();
  170. const currentPage = pages[pages.length - 1];
  171. if (currentPage.options && currentPage.options.id) {
  172. isEdit.value = true;
  173. editId.value = currentPage.options.id;
  174. fetchNoticeDetail(currentPage.options.id);
  175. }
  176. });
  177. const fetchNoticeDetail = async (id) => {
  178. try {
  179. const res = await overviewApi.notices_edit({ id });
  180. if (res.data) {
  181. const data = res.data;
  182. console.log('userList原始数据:', data.userList);
  183. formData.title = data.noticeName || '';
  184. const content = data.noticeContent || '';
  185. formData.content = content.replace(/<[^>]+>/g, '');
  186. formData.status = data.isClose !== true;
  187. noticeTypeId.value = data.typeId || '';
  188. const typeIndexVal = typeData.value.findIndex(item => item.id === data.typeId);
  189. typeIndex.value = typeIndexVal >= 0 ? typeIndexVal : -1;
  190. publishType.value = data.sendTimeType === 2 ? 1 : 0;
  191. publishDateTime.value = data.releaseTime || getCurrentDateTime();
  192. saveRecord.value = data.isSave === true ? 0 : 1;
  193. if (data.noticeType) {
  194. const typeMap = {
  195. 1: '按部门',
  196. 2: '按学科',
  197. 3: '按年级',
  198. 4: '按权限',
  199. 5: '按学生'
  200. };
  201. objectTypeLabel.value = typeMap[data.noticeType] || '';
  202. originalNoticeType.value = data.noticeType;
  203. }
  204. if (data.userList && Array.isArray(data.userList)) {
  205. selectedUsers.value = data.userList.map(user => {
  206. const thirdCode = user.thirdCode || user.userAccount || user.teacherId || user.studentCode || '';
  207. return {
  208. id: user.userId,
  209. name: user.thirdName,
  210. thirdCode: thirdCode,
  211. userAccount: user.userAccount || thirdCode
  212. };
  213. });
  214. selectedCount.value = selectedUsers.value.length;
  215. }
  216. }
  217. } catch (error) {
  218. console.error('获取通知详情失败:', error);
  219. }
  220. };
  221. onShow(() => {
  222. const result = uni.getStorageSync('selectObjectResult');
  223. if (result) {
  224. try {
  225. const data = JSON.parse(result);
  226. if (data) {
  227. objectTypeLabel.value = data.tab;
  228. selectedCount.value = data.count;
  229. selectedUsers.value = data.users;
  230. }
  231. } catch (e) {
  232. console.error('解析选择结果失败:', e);
  233. }
  234. uni.removeStorageSync('selectObjectResult');
  235. }
  236. const personResult = uni.getStorageSync('personListResult');
  237. if (personResult) {
  238. try {
  239. const data = JSON.parse(personResult);
  240. if (data && data.users) {
  241. selectedUsers.value = data.users;
  242. selectedCount.value = data.users.length;
  243. }
  244. } catch (e) {
  245. console.error('解析人员列表结果失败:', e);
  246. }
  247. uni.removeStorageSync('personListResult');
  248. }
  249. });
  250. const handleTypeChange = (e) => {
  251. typeIndex.value = e.detail.value;
  252. const selectedItem = typeData.value[typeIndex.value];
  253. if (selectedItem) {
  254. formData.noticeType = selectedItem.name;
  255. noticeTypeId.value = selectedItem.id;
  256. }
  257. };
  258. const goBack = () => {
  259. uni.redirectTo({ url: '/pages/notice/overview/index' });
  260. };
  261. const onStatusChange = (e) => {
  262. formData.status = e.detail.value;
  263. if (formData.status) {
  264. saveRecord.value = 0;
  265. }
  266. };
  267. const selectObject = () => {
  268. const lastSelection = {
  269. tab: objectTypeLabel.value,
  270. users: selectedUsers.value,
  271. noticeType: originalNoticeType.value || getNoticeTypeValue(objectTypeLabel.value),
  272. isEdit: isEdit.value
  273. };
  274. uni.setStorageSync('selectObjectLastSelection', JSON.stringify(lastSelection));
  275. uni.navigateTo({ url: '/pages/notice/publish/selectObject' });
  276. };
  277. const goPersonList = () => {
  278. if (selectedUsers.value.length === 0) {
  279. return;
  280. }
  281. const data = {
  282. tab: objectTypeLabel.value,
  283. users: selectedUsers.value
  284. };
  285. uni.setStorageSync('personListData', JSON.stringify(data));
  286. uni.navigateTo({ url: '/pages/notice/publish/personList' });
  287. };
  288. const handleObjectSelect = (data) => {
  289. if (data) {
  290. objectTypeLabel.value = data.tab;
  291. selectedCount.value = data.count;
  292. selectedUsers.value = data.users;
  293. }
  294. };
  295. const getCurrentTime = () => {
  296. const now = new Date();
  297. const year = now.getFullYear();
  298. const month = String(now.getMonth() + 1).padStart(2, '0');
  299. const day = String(now.getDate()).padStart(2, '0');
  300. const hours = String(now.getHours()).padStart(2, '0');
  301. const minutes = String(now.getMinutes()).padStart(2, '0');
  302. const seconds = String(now.getSeconds()).padStart(2, '0');
  303. return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
  304. };
  305. const getNoticeTypeValue = (tab) => {
  306. const map = {
  307. '按部门': 1,
  308. '按学科': 2,
  309. '按年级': 3,
  310. '按权限': 4,
  311. '按学生': 5
  312. };
  313. return map[tab] || 1;
  314. };
  315. const handleSave = async () => {
  316. if (!formData.title) {
  317. uni.showToast({ title: '请输入通知标题', icon: 'none' });
  318. return;
  319. }
  320. if (!formData.content) {
  321. uni.showToast({ title: '请输入通知内容', icon: 'none' });
  322. return;
  323. }
  324. if (!objectTypeLabel.value) {
  325. uni.showToast({ title: '请选择通知对象', icon: 'none' });
  326. return;
  327. }
  328. if (selectedUsers.value.length === 0) {
  329. uni.showToast({ title: '请选择通知人员', icon: 'none' });
  330. return;
  331. }
  332. if (!schoolYearId.value) {
  333. try {
  334. const res = await overviewApi.findSchoolYear();
  335. if (res.data && res.data.length > 0) {
  336. schoolYearId.value = res.data[0].id;
  337. }
  338. } catch (error) {
  339. console.error('获取学年失败:', error);
  340. uni.showToast({ title: '获取学年信息失败', icon: 'none' });
  341. return;
  342. }
  343. }
  344. uni.showLoading({ title: '发布中...' });
  345. try {
  346. const objectType = getNoticeTypeValue(objectTypeLabel.value);
  347. const params = {
  348. typeId: noticeTypeId.value || '',
  349. noticeName: formData.title,
  350. noticeContent: btoa(unescape(encodeURIComponent(formData.content))),
  351. sendTimeType: publishType.value + 1,
  352. releaseTime: publishType.value === 0 ? getCurrentTime() : publishDateTime.value,
  353. noticeType: objectType,
  354. isClose: isEdit.value ? !formData.status : false,
  355. isSave: isEdit.value ? (saveRecord.value === 0) : false,
  356. fileList: []
  357. };
  358. if (isEdit.value && editId.value) {
  359. params.id = editId.value;
  360. }
  361. const userList = selectedUsers.value.map(user => {
  362. const userInfo = {
  363. userType: objectType === 5 ? 2 : 1,
  364. userId: user.id,
  365. thirdName: user.name,
  366. thirdCode: user.thirdCode || user.userAccount || '',
  367. gradeName: '',
  368. classTypeName: '',
  369. className: '',
  370. departmentName: '',
  371. groupName: '',
  372. subjectName: '',
  373. roleName: ''
  374. };
  375. switch (objectType) {
  376. case 1:
  377. userInfo.departmentName = user.departmentName || '';
  378. userInfo.groupName = user.groupName || '';
  379. break;
  380. case 2:
  381. userInfo.subjectName = user.subjectName || '';
  382. userInfo.gradeName = user.gradeName || '';
  383. break;
  384. case 3:
  385. userInfo.gradeName = user.gradeName || '';
  386. userInfo.classTypeName = user.classTypeName || '';
  387. userInfo.className = user.className || '';
  388. break;
  389. case 4:
  390. userInfo.roleName = user.roleName || '';
  391. break;
  392. }
  393. return userInfo;
  394. });
  395. if (userList.length > 0) {
  396. params.userList = userList;
  397. }
  398. const res = await overviewApi.save_notices(params);
  399. if (res.code === 200) {
  400. uni.showToast({ title: '发布成功', icon: 'success' });
  401. setTimeout(() => {
  402. uni.redirectTo({ url: '/pages/notice/overview/index' });
  403. }, 1500);
  404. } else {
  405. uni.showToast({ title: res.message || '发布失败', icon: 'none' });
  406. }
  407. } catch (error) {
  408. console.error('发布失败:', error);
  409. uni.showToast({ title: '发布失败', icon: 'none' });
  410. } finally {
  411. uni.hideLoading();
  412. }
  413. };
  414. </script>
  415. <style lang="scss" scoped>
  416. .page_publish {
  417. // min-height: 100vh;
  418. // background-color: #F5F5F5;
  419. display: flex;
  420. flex-direction: column;
  421. }
  422. .publish_header {
  423. position: fixed;
  424. top: 0;
  425. left: 0;
  426. right: 0;
  427. z-index: 100;
  428. display: flex;
  429. align-items: center;
  430. justify-content: space-between;
  431. height: 96rpx;
  432. padding: 0 24rpx;
  433. background-color: #FFFFFF;
  434. border-bottom: 2rpx solid #F3F3F3;
  435. .header_title {
  436. font-weight: 500;
  437. font-size: 32rpx;
  438. color: #333333;
  439. }
  440. .header_placeholder {
  441. width: 48rpx;
  442. }
  443. }
  444. .publish_content {
  445. flex: 1;
  446. box-sizing: border-box;
  447. padding: 120rpx 24rpx 158rpx 24rpx;
  448. }
  449. .form_item {
  450. background-color: #FFFFFF;
  451. border-radius: 8rpx;
  452. padding-bottom: 24rpx;
  453. margin-bottom: 24rpx;
  454. border-bottom: 2rpx solid #F3F3F3;
  455. &:last-child {
  456. border-bottom: none;
  457. }
  458. .form_label {
  459. font-size: 28rpx;
  460. color: #666666;
  461. font-weight: 400;
  462. white-space: nowrap;
  463. flex-shrink: 0;
  464. margin-right: 16rpx;
  465. &::after {
  466. content: ':';
  467. margin-left: 8rpx;
  468. }
  469. }
  470. // 通知类型
  471. .form_row {
  472. display: flex;
  473. align-items: flex-start;
  474. .form_content {
  475. flex: 1;
  476. }
  477. }
  478. .form_row_type {
  479. align-items: center;
  480. .filter_item {
  481. display: flex;
  482. align-items: center;
  483. justify-content: space-between;
  484. padding: 0 16rpx;
  485. height: 72rpx;
  486. line-height: 72rpx;
  487. background-color: #FFFFFF;
  488. border-radius: 8rpx;
  489. width: 240rpx;
  490. border: 2rpx solid #DCDFE6;
  491. .filter_text {
  492. font-size: 28rpx;
  493. color: #333333;
  494. margin-right: 8rpx;
  495. &.placeholder {
  496. color: #909399;
  497. }
  498. }
  499. }
  500. }
  501. .form_input {
  502. font-size: 28rpx;
  503. color: #333333;
  504. padding: 0 24rpx;
  505. background-color: #FFFFFF;
  506. border-radius: 8rpx;
  507. width: 100%;
  508. height: 72rpx;
  509. box-sizing: border-box;
  510. border: 2rpx solid #E9E9E9;
  511. }
  512. .form_select {
  513. display: flex;
  514. align-items: center;
  515. justify-content: space-between;
  516. padding: 16rpx;
  517. background-color: #F5F7FA;
  518. border-radius: 8rpx;
  519. border: 2rpx solid #DCDFE6;
  520. .select_text {
  521. font-size: 28rpx;
  522. color: #333333;
  523. }
  524. .select_right {
  525. .select_arrow {
  526. font-size: 28rpx;
  527. color: #999999;
  528. }
  529. }
  530. }
  531. .textarea_container {
  532. position: relative;
  533. .form_textarea {
  534. width: 100%;
  535. height: 240rpx;
  536. font-size: 28rpx;
  537. color: #333333;
  538. line-height: 1.6;
  539. padding: 14rpx 24rpx 56rpx 24rpx;
  540. background-color: #FFFFFF;
  541. border-radius: 8rpx;
  542. box-sizing: border-box;
  543. border: 2rpx solid #E9E9E9;
  544. }
  545. .textarea_actions {
  546. position: absolute;
  547. bottom: 13rpx;
  548. right: 16rpx;
  549. display: flex;
  550. align-items: center;
  551. gap: 8rpx;
  552. .full_icon {
  553. width: 28rpx;
  554. height: 28rpx;
  555. }
  556. .action_text {
  557. font-weight: 400;
  558. font-size: 28rpx;
  559. color: #2E64FA;
  560. }
  561. }
  562. }
  563. .time_picker {
  564. margin-top: 20rpx;
  565. min-width: 300rpx;
  566. height: 68rpx;
  567. float: right;
  568. :deep(.uni-date-x--border) {
  569. height: 68rpx;
  570. }
  571. :deep(.uni-date-x) {
  572. padding: 0 16rpx;
  573. height: 48rpx;
  574. }
  575. :deep(.uni-date-x--text) {
  576. font-weight: 400;
  577. font-size: 28rpx;
  578. color: #333333;
  579. }
  580. :deep(.uni-date__x-input) {
  581. font-size: 28rpx;
  582. color: #333333;
  583. }
  584. }
  585. .object_row {
  586. display: flex;
  587. align-items: center;
  588. justify-content: space-between;
  589. .object_info {
  590. display: flex;
  591. align-items: center;
  592. gap: 12rpx;
  593. .object_count {
  594. font-size: 28rpx;
  595. color: #2E64FA;
  596. }
  597. }
  598. .object_action {
  599. display: flex;
  600. align-items: center;
  601. gap: 8rpx;
  602. .action_text {
  603. font-size: 28rpx;
  604. color: #2E64FA;
  605. }
  606. .action_arrow {
  607. font-size: 28rpx;
  608. color: #2E64FA;
  609. }
  610. }
  611. }
  612. .object_tip {
  613. font-size: 24rpx;
  614. color: #F56C6C;
  615. margin-top: 12rpx;
  616. display: block;
  617. }
  618. .switch_row {
  619. display: flex;
  620. flex-direction:column;
  621. // gap: 16rpx;
  622. .switch_container {
  623. transform: scale(0.8);
  624. transform-origin: left center;
  625. }
  626. .switch_text {
  627. font-weight: 400;
  628. font-size: 28rpx;
  629. color: #999999;
  630. }
  631. }
  632. .record_tip {
  633. font-weight: 400;
  634. font-size: 28rpx;
  635. color: #999999;
  636. margin-top: 16rpx;
  637. display: block;
  638. }
  639. }
  640. .publish_footer {
  641. padding: 24rpx;
  642. padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
  643. background-color: #FFFFFF;
  644. .confirm_btn {
  645. width: 702rpx;
  646. height: 90rpx;
  647. background: #2E64FA;
  648. border-radius: 8rpx 8rpx 8rpx 8rpx;
  649. text-align: center;
  650. line-height: 90rpx;
  651. .btn_text {
  652. font-size: 32rpx;
  653. font-weight: 500;
  654. color: #FFFFFF;
  655. }
  656. }
  657. }
  658. </style>