noticeHeader.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  1. <template>
  2. <view class="notice_header" :style="{ paddingTop: statusBarHeight + 'rpx' }">
  3. <!-- 顶部栏 -->
  4. <view class="header_top">
  5. <view class="header_left">
  6. <uni-icons type="back" size="24" color="#333333" @click="goBack"></uni-icons>
  7. <text class="header_title">{{ title }}</text>
  8. </view>
  9. <view v-if="showSearch" class="search_box">
  10. <image src="/static/image/overview/search.png" class="search_icon" ></image>
  11. <input type="text" class="search_input" placeholder="请输入关键字搜索" placeholder-class="search_placeholder" v-model="searchWord" @input="handleSearchInput" />
  12. </view>
  13. <text v-else-if="showSave" class="save_btn" @click="handleSave">保存</text>
  14. </view>
  15. <!-- 通知模式过滤栏 -->
  16. <view v-if="showFilter && mode === 'notice'" class="filter_bar">
  17. <picker mode="selector" :range="typeOptions" :value="typeIndex" @change="handleTypeChange">
  18. <view class="filter_item">
  19. <text class="filter_text">{{ typeOptions[typeIndex] }}</text>
  20. <uni-icons type="down" size="16" color="#999999"></uni-icons>
  21. </view>
  22. </picker>
  23. <picker mode="selector" :range="timeOptions" :value="timeIndex" @change="handleTimeChange">
  24. <view class="filter_item">
  25. <text class="filter_text">{{ timeOptions[timeIndex] }}</text>
  26. <uni-icons type="down" size="16" color="#999999"></uni-icons>
  27. </view>
  28. </picker>
  29. </view>
  30. <!-- 通用过滤栏(任务总览/材料采集模式) -->
  31. <view v-if="showFilter && (mode === 'taskOverview' || mode === 'material')" class="filter_bar task_overview_filter">
  32. <picker mode="selector" class="picker_wide" :range="academicYearOptions" :value="academicYearIndex" @change="handleAcademicYearChange">
  33. <view class="filter_item">
  34. <text class="filter_text">{{ academicYearOptions[academicYearIndex] }}</text>
  35. <uni-icons type="down" size="16" color="#999999"></uni-icons>
  36. </view>
  37. </picker>
  38. <picker mode="selector" class="picker_narrow" :range="categoryOptions" :value="categoryIndex" @change="handleCategoryChange">
  39. <view class="filter_item">
  40. <text class="filter_text">{{ categoryOptions[categoryIndex] }}</text>
  41. <uni-icons type="down" size="16" color="#999999"></uni-icons>
  42. </view>
  43. </picker>
  44. <picker mode="selector" class="picker_narrow" :range="materialTypeOptions" :value="materialTypeIndex" @change="handleMaterialTypeChange">
  45. <view class="filter_item">
  46. <text class="filter_text">{{ materialTypeOptions[materialTypeIndex] }}</text>
  47. <uni-icons type="down" size="16" color="#999999"></uni-icons>
  48. </view>
  49. </picker>
  50. </view>
  51. <!-- 材料采集模式阶段按钮 -->
  52. <view v-if="showStageBtn && mode === 'material'" class="stage_btn_bar">
  53. <view
  54. class="stage_btn"
  55. :class="{ active: stageIndex === 0 }"
  56. @click="handleStageChange(0)"
  57. >
  58. <text>进行中</text>
  59. </view>
  60. <view
  61. class="stage_btn"
  62. :class="{ active: stageIndex === 1 }"
  63. @click="handleStageChange(1)"
  64. >
  65. <text>已结束</text>
  66. </view>
  67. </view>
  68. <!-- 通用标签栏(三种模式通用) -->
  69. <view
  70. v-if="(showTabs || (showCreateBtn && mode === 'taskOverview'))"
  71. class="tabs_bar"
  72. :class="{
  73. 'task_overview_tabs': mode === 'taskOverview',
  74. 'material_tabs': mode === 'material'
  75. }"
  76. >
  77. <text
  78. v-for="(tab, idx) in tabs"
  79. :key="idx"
  80. class="tab_item"
  81. :class="{ active: activeTabIndex === idx }"
  82. @click="handleTabChange(idx)"
  83. >{{ tab }}</text>
  84. <view v-if="showCreateBtn && mode === 'taskOverview'" class="create_btn" @click="handleCreate">
  85. <image src="/static/image/workspace/add.png" class="add_icon" mode="aspectFit"></image>
  86. <text class="create_text">新建</text>
  87. </view>
  88. </view>
  89. </view>
  90. </template>
  91. <script setup>
  92. import { ref, onMounted, computed, watch } from 'vue';
  93. import uniIcons from '@/uni_modules/uni-icons/components/uni-icons/uni-icons.vue';
  94. import overviewApi from '@/reqApi/overview.js';
  95. import { useSafeArea } from '@/common/safeArea';
  96. const { statusBarHeight, initSafeArea } = useSafeArea();
  97. const typeIndex = ref(0);
  98. const timeIndex = ref(0);
  99. const typeData = ref([{ id: '0', name: '全部类型' }]);
  100. const typeOptions = ref(['全部类型']);
  101. const searchWord = ref('');
  102. const academicYearIndex = ref(0);
  103. const categoryIndex = ref(0);
  104. const materialTypeIndex = ref(0);
  105. const categoryDataList = ref([]); // 存储分类树形数据(来自prop)
  106. const categoryOptions = ref(['全部分类']); // 分类选项
  107. const materialTypeOptions = ref(['全部类目']); // 类目选项(当前选中分类的childList)
  108. const stageIndex = ref(0);
  109. const props = defineProps({
  110. title: {
  111. type: String,
  112. default: ''
  113. },
  114. mode: {
  115. type: String,
  116. default: 'notice',
  117. validator: (value) => ['notice', 'taskOverview', 'material'].includes(value)
  118. },
  119. showSearch: {
  120. type: Boolean,
  121. default: false
  122. },
  123. showSave: {
  124. type: Boolean,
  125. default: false
  126. },
  127. showFilter: {
  128. type: Boolean,
  129. default: false
  130. },
  131. showStageBtn: {
  132. type: Boolean,
  133. default: false
  134. },
  135. timeOptions: {
  136. type: Array,
  137. default: () => ['全部时间', '最近3天', '最近7天', '最近1月', '最近3月']
  138. },
  139. academicYearOptions: {
  140. type: Array,
  141. default: () => ['']
  142. },
  143. academicYearIds: {
  144. type: Array,
  145. default: () => ['']
  146. },
  147. categoryData: {
  148. type: Array,
  149. default: () => []
  150. },
  151. showTabs: {
  152. type: Boolean,
  153. default: false
  154. },
  155. showCreateBtn: {
  156. type: Boolean,
  157. default: false
  158. },
  159. tabs: {
  160. type: Array,
  161. default: () => ['全部', '待发布', '已发布', '已关闭']
  162. },
  163. activeTab: {
  164. type: Number,
  165. default: -1
  166. },
  167. tabMode: {
  168. type: String,
  169. default: 'status',
  170. validator: (value) => ['status', 'index'].includes(value)
  171. }
  172. });
  173. const activeTabIndex = computed(() => {
  174. if (props.tabMode === 'index') {
  175. return props.activeTab;
  176. }
  177. const statusMap = [-1, 0, 1, 2];
  178. return statusMap.indexOf(props.activeTab);
  179. });
  180. const emit = defineEmits([
  181. 'save', 'tabChange', 'typeChange', 'timeChange', 'searchChange',
  182. 'academicYearChange', 'categoryChange', 'materialTypeChange', 'create',
  183. 'stageChange'
  184. ]);
  185. const fetchNoticeTypeList = async () => {
  186. try {
  187. const res = await overviewApi.noticeTypeList();
  188. if (res.data && Array.isArray(res.data)) {
  189. const types = res.data.map(item => ({
  190. id: item.id,
  191. name: item.name
  192. }));
  193. typeData.value = [{ id: '0', name: '全部类型' }, ...types];
  194. typeOptions.value = ['全部类型', ...types.map(item => item.name)];
  195. }
  196. } catch (error) {
  197. console.error('获取通知类型失败:', error);
  198. }
  199. };
  200. onMounted(() => {
  201. initSafeArea();
  202. if (props.mode === 'notice') {
  203. fetchNoticeTypeList();
  204. }
  205. });
  206. // 监听学年学期数据,默认选中第一条实际数据(跳过"全部")
  207. watch(() => props.academicYearOptions, (newVal) => {
  208. if (newVal && newVal.length > 0) {
  209. academicYearIndex.value = 0;
  210. }
  211. }, { immediate: true });
  212. // 监听分类数据,填充分类选项
  213. watch(() => props.categoryData, (newVal) => {
  214. if (newVal && Array.isArray(newVal)) {
  215. categoryDataList.value = newVal;
  216. // 分类选项:全部分类 + 一级分类名称
  217. const categoryNames = newVal.map(item => item.categoryName);
  218. categoryOptions.value = ['全部分类', ...categoryNames];
  219. // 默认选中全部分类,类目为空
  220. categoryIndex.value = 0;
  221. materialTypeOptions.value = ['全部类目'];
  222. }
  223. }, { immediate: true });
  224. const handleTabChange = (idx) => {
  225. if (props.tabMode === 'index') {
  226. emit('tabChange', idx);
  227. } else {
  228. const statusMap = [-1, 0, 1, 2];
  229. emit('tabChange', statusMap[idx]);
  230. }
  231. };
  232. const handleTypeChange = (e) => {
  233. typeIndex.value = e.detail.value;
  234. const selectedItem = typeData.value[typeIndex.value];
  235. emit('typeChange', selectedItem.id);
  236. };
  237. const handleTimeChange = (e) => {
  238. timeIndex.value = e.detail.value;
  239. emit('timeChange', timeIndex.value);
  240. };
  241. const handleAcademicYearChange = (e) => {
  242. academicYearIndex.value = e.detail.value;
  243. const selectedId = props.academicYearIds[academicYearIndex.value] || '';
  244. emit('academicYearChange', academicYearIndex.value, selectedId);
  245. };
  246. const handleCategoryChange = (e) => {
  247. categoryIndex.value = e.detail.value;
  248. materialTypeIndex.value = 0;
  249. if (categoryIndex.value === 0) {
  250. // 全部分类:类目默认只有"全部类目"
  251. materialTypeOptions.value = ['全部类目'];
  252. emit('categoryChange', categoryIndex.value, '');
  253. } else {
  254. // 具体分类:加载该分类下的 childList 作为类目选项(前面加"全部类目")
  255. const selectedCategory = categoryDataList.value[categoryIndex.value - 1];
  256. const childList = selectedCategory?.childList || [];
  257. materialTypeOptions.value = ['全部类目', ...childList.map(child => child.categoryName)];
  258. emit('categoryChange', categoryIndex.value, selectedCategory?.id || '');
  259. }
  260. };
  261. const handleMaterialTypeChange = (e) => {
  262. materialTypeIndex.value = e.detail.value;
  263. if (categoryIndex.value === 0) {
  264. // 全部分类时,类目只有"全部类目"
  265. emit('materialTypeChange', materialTypeIndex.value, '');
  266. } else {
  267. const selectedCategory = categoryDataList.value[categoryIndex.value - 1];
  268. const childList = selectedCategory?.childList || [];
  269. // index=0 是"全部类目",无对应ID
  270. const selectedChildId = materialTypeIndex.value === 0 ? '' : (childList[materialTypeIndex.value - 1]?.id || '');
  271. emit('materialTypeChange', materialTypeIndex.value, selectedChildId);
  272. }
  273. };
  274. const goBack = () => {
  275. uni.redirectTo({
  276. url: '/pages/workspace/index'
  277. });
  278. };
  279. const handleSave = () => {
  280. emit('save');
  281. };
  282. const handleSearchInput = () => {
  283. emit('searchChange', searchWord.value);
  284. };
  285. const handleCreate = () => {
  286. emit('create');
  287. };
  288. const handleStageChange = (idx) => {
  289. stageIndex.value = idx;
  290. emit('stageChange', idx);
  291. };
  292. </script>
  293. <style lang="scss" scoped>
  294. .notice_header {
  295. position: fixed;
  296. top: 0;
  297. left: 0;
  298. right: 0;
  299. display: flex;
  300. flex-direction: column;
  301. background-color: #FFFFFF;
  302. z-index: 999;
  303. .header_top {
  304. display: flex;
  305. align-items: center;
  306. justify-content: space-between;
  307. padding:56rpx 24rpx 24rpx;
  308. .header_left {
  309. display: flex;
  310. align-items: center;
  311. .header_title {
  312. font-weight: 600;
  313. font-size: 44rpx;
  314. color: #333333;
  315. margin-left: 24rpx;
  316. }
  317. }
  318. .search_box {
  319. display: flex;
  320. align-items: center;
  321. background-color: #fff;
  322. border-radius: 8rpx;
  323. padding: 0rpx 24rpx;
  324. width: 265rpx;
  325. border: 2rpx solid #DCDFE6;
  326. height: 72rpx;
  327. line-height: 72rpx;
  328. .search_icon {
  329. width: 32rpx;
  330. height: 32rpx;
  331. margin-right: 8rpx;
  332. }
  333. .search_input {
  334. flex: 1;
  335. font-size: 24rpx;
  336. color: #333333;
  337. margin-left: 8rpx;
  338. }
  339. .search_placeholder {
  340. color: #999999;
  341. }
  342. }
  343. .save_btn {
  344. font-size: 28rpx;
  345. color: #2E64FA;
  346. font-weight: 500;
  347. }
  348. }
  349. .filter_bar {
  350. display: flex;
  351. gap: 32rpx;
  352. padding: 20rpx 24rpx;
  353. background-color: #FFFFFF;
  354. border-top: 1rpx solid #F0F0F0;
  355. .filter_item {
  356. display: flex;
  357. align-items: center;
  358. justify-content: space-between;
  359. padding: 20rpx 16rpx;
  360. background-color: #FFFFFF;
  361. border-radius: 8rpx;
  362. min-width: 240rpx;
  363. border: 2rpx solid #DCDFE6;
  364. .filter_text {
  365. font-size: 28rpx;
  366. color: #333333;
  367. margin-right: 8rpx;
  368. }
  369. }
  370. }
  371. .task_overview_filter {
  372. gap: 16rpx;
  373. .picker_wide {
  374. flex: 1.2;
  375. .filter_text{
  376. max-width: 240rpx !important;
  377. }
  378. }
  379. .picker_narrow {
  380. flex: 1;
  381. }
  382. .filter_item {
  383. width: 100%;
  384. min-width: 0;
  385. padding: 16rpx 12rpx;
  386. box-sizing: border-box;
  387. .filter_text {
  388. font-size: 24rpx;
  389. overflow: hidden;
  390. text-overflow: ellipsis;
  391. white-space: nowrap;
  392. max-width: 140rpx;
  393. margin-right: 0rpx;
  394. }
  395. }
  396. }
  397. .tabs_bar {
  398. display: flex;
  399. height: 56rpx;
  400. padding: 4rpx 24rpx 12rpx 24rpx;
  401. .tab_item {
  402. font-size: 32rpx;
  403. color: #999999;
  404. font-weight: 400;
  405. position: relative;
  406. margin-right: 48rpx;
  407. &.active {
  408. color: #2E64FA;
  409. font-weight: 500;
  410. &::after {
  411. content: '';
  412. position: absolute;
  413. bottom: 0rpx;
  414. left: 50%;
  415. transform: translateX(-50%);
  416. width: 100%;
  417. height: 4rpx;
  418. background-color: #2E64FA;
  419. }
  420. }
  421. }
  422. }
  423. .task_overview_tabs {
  424. justify-content: flex-start;
  425. align-items: center;
  426. height: 72rpx;
  427. .tab_item {
  428. margin-right: 48rpx;
  429. &.active {
  430. &::after {
  431. bottom: -8rpx;
  432. }
  433. }
  434. }
  435. .create_btn {
  436. margin-left: auto;
  437. background-color: #2E64FA;
  438. border-radius: 8rpx;
  439. width: 148rpx;
  440. height: 72rpx;
  441. display: flex;
  442. align-items: center;
  443. justify-content: center;
  444. gap: 8rpx;
  445. .add_icon {
  446. width: 28rpx;
  447. height: 28rpx;
  448. }
  449. .create_text {
  450. color: #FFFFFF;
  451. font-size: 28rpx;
  452. font-weight: 500;
  453. }
  454. }
  455. }
  456. // 材料采集模式 - 阶段按钮
  457. .stage_btn_bar {
  458. display: flex;
  459. gap: 30rpx;
  460. padding:0rpx 24rpx 24rpx 24rpx;
  461. .stage_btn {
  462. flex: 1;
  463. display: flex;
  464. align-items: center;
  465. justify-content: center;
  466. height: 64rpx;
  467. border-radius: 8rpx;
  468. font-size: 28rpx;
  469. color: #333333;
  470. border: 2rpx solid #DCDFE6;
  471. border-radius: 8rpx 8rpx 8rpx 8rpx;
  472. &.active {
  473. background: rgba(46,100,250,0.1);
  474. color: #2E64FA;
  475. border: 2rpx solid #2E64FA;
  476. }
  477. }
  478. }
  479. // 材料采集模式 - 标签栏
  480. .material_tabs {
  481. height: 48rpx;
  482. padding: 4rpx 24rpx 16rpx 24rpx;
  483. .tab_item {
  484. font-size: 28rpx;
  485. margin-right: 48rpx;
  486. color: #999999;
  487. font-weight: 400;
  488. &.active {
  489. color: #2E64FA;
  490. font-weight: 500;
  491. }
  492. }
  493. }
  494. }
  495. </style>