refactor(plan): 替换 Taro.showActionSheet 为 NutUI ActionSheet 组件
将计划书多文件选择从原生 Taro.showActionSheet 迁移至 ProposalFileActionSheet 组件,支持长文件名换行显示和后续 样式扩展。usePlanView 暴露响应式 actionSheetVisible 等状态 供组件绑定,plan 与 message-detail 页面各挂载一份实例。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Showing
6 changed files
with
118 additions
and
21 deletions
| ... | @@ -21,6 +21,7 @@ declare module 'vue' { | ... | @@ -21,6 +21,7 @@ declare module 'vue' { |
| 21 | MaterialCard: typeof import('./src/components/cards/MaterialCard.vue')['default'] | 21 | MaterialCard: typeof import('./src/components/cards/MaterialCard.vue')['default'] |
| 22 | NameInput: typeof import('./src/components/plan/PlanFields/NameInput.vue')['default'] | 22 | NameInput: typeof import('./src/components/plan/PlanFields/NameInput.vue')['default'] |
| 23 | NavHeader: typeof import('./src/components/navigation/NavHeader.vue')['default'] | 23 | NavHeader: typeof import('./src/components/navigation/NavHeader.vue')['default'] |
| 24 | + NutActionSheet: typeof import('@nutui/nutui-taro')['ActionSheet'] | ||
| 24 | NutAvatar: typeof import('@nutui/nutui-taro')['Avatar'] | 25 | NutAvatar: typeof import('@nutui/nutui-taro')['Avatar'] |
| 25 | NutButton: typeof import('@nutui/nutui-taro')['Button'] | 26 | NutButton: typeof import('@nutui/nutui-taro')['Button'] |
| 26 | NutDatePicker: typeof import('@nutui/nutui-taro')['DatePicker'] | 27 | NutDatePicker: typeof import('@nutui/nutui-taro')['DatePicker'] |
| ... | @@ -40,6 +41,7 @@ declare module 'vue' { | ... | @@ -40,6 +41,7 @@ declare module 'vue' { |
| 40 | PlanFormContainer: typeof import('./src/components/plan/PlanFormContainer.vue')['default'] | 41 | PlanFormContainer: typeof import('./src/components/plan/PlanFormContainer.vue')['default'] |
| 41 | PlanPopupNew: typeof import('./src/components/plan/PlanPopupNew.vue')['default'] | 42 | PlanPopupNew: typeof import('./src/components/plan/PlanPopupNew.vue')['default'] |
| 42 | ProductCard: typeof import('./src/components/cards/ProductCard.vue')['default'] | 43 | ProductCard: typeof import('./src/components/cards/ProductCard.vue')['default'] |
| 44 | + ProposalFileActionSheet: typeof import('./src/components/plan/ProposalFileActionSheet.vue')['default'] | ||
| 43 | RadioGroup: typeof import('./src/components/plan/PlanFields/RadioGroup.vue')['default'] | 45 | RadioGroup: typeof import('./src/components/plan/PlanFields/RadioGroup.vue')['default'] |
| 44 | RichTextRenderer: typeof import('./src/components/RichTextRenderer.vue')['default'] | 46 | RichTextRenderer: typeof import('./src/components/RichTextRenderer.vue')['default'] |
| 45 | RouterLink: typeof import('vue-router')['RouterLink'] | 47 | RouterLink: typeof import('vue-router')['RouterLink'] | ... | ... |
| 1 | +<template> | ||
| 2 | + <nut-action-sheet | ||
| 3 | + v-model:visible="actionSheetVisible" | ||
| 4 | + :title="sheetTitle" | ||
| 5 | + cancel-txt="取消" | ||
| 6 | + :menu-items="actionSheetMenuItems" | ||
| 7 | + :close-on-click-overlay="true" | ||
| 8 | + @choose="handleChooseFile" | ||
| 9 | + @cancel="closeActionSheet" | ||
| 10 | + @close="closeActionSheet" | ||
| 11 | + /> | ||
| 12 | +</template> | ||
| 13 | + | ||
| 14 | +<script setup> | ||
| 15 | +/** | ||
| 16 | + * 计划书文件选择动作面板 | ||
| 17 | + * | ||
| 18 | + * @description 多文件计划书统一使用 NutUI ActionSheet 展示, | ||
| 19 | + * 以支持长文件名换行和后续样式扩展。 | ||
| 20 | + */ | ||
| 21 | +import { computed } from 'vue' | ||
| 22 | +import { usePlanView } from '@/composables/usePlanView' | ||
| 23 | + | ||
| 24 | +const { actionSheetVisible, actionSheetMenuItems, handleChooseFile, closeActionSheet } = usePlanView() | ||
| 25 | + | ||
| 26 | +const sheetTitle = computed(() => { | ||
| 27 | + const count = actionSheetMenuItems.value.length | ||
| 28 | + return count > 0 ? `选择计划书文件 (${count}个)` : '选择计划书文件' | ||
| 29 | +}) | ||
| 30 | +</script> | ||
| 31 | + | ||
| 32 | +<style lang="less" scoped> | ||
| 33 | +:deep(.nut-action-sheet__item) { | ||
| 34 | + padding: 24rpx 32rpx; | ||
| 35 | + text-align: left; | ||
| 36 | + white-space: normal; | ||
| 37 | + word-break: break-word; | ||
| 38 | +} | ||
| 39 | + | ||
| 40 | +:deep(.nut-action-sheet__item > view:first-child) { | ||
| 41 | + font-weight: 500; | ||
| 42 | + color: #1f2937; | ||
| 43 | + white-space: normal; | ||
| 44 | + word-break: break-word; | ||
| 45 | + line-height: 1.5; | ||
| 46 | +} | ||
| 47 | + | ||
| 48 | +:deep(.nut-action-sheet__subdesc) { | ||
| 49 | + margin-top: 8rpx; | ||
| 50 | + line-height: 1.5; | ||
| 51 | + color: #6b7280; | ||
| 52 | + word-break: break-word; | ||
| 53 | +} | ||
| 54 | +</style> |
| ... | @@ -10,7 +10,7 @@ | ... | @@ -10,7 +10,7 @@ |
| 10 | import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' | 10 | import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' |
| 11 | import { ref, reactive } from 'vue' | 11 | import { ref, reactive } from 'vue' |
| 12 | import Taro from '@tarojs/taro' | 12 | import Taro from '@tarojs/taro' |
| 13 | -import { viewProposal } from '../usePlanView' | 13 | +import { usePlanView, viewProposal } from '../usePlanView' |
| 14 | import { useFieldValueTransform } from '../useFieldValueTransform' | 14 | import { useFieldValueTransform } from '../useFieldValueTransform' |
| 15 | import { useFieldDependencies } from '../useFieldDependencies' | 15 | import { useFieldDependencies } from '../useFieldDependencies' |
| 16 | import { PLAN_FIELD_DEFINITIONS, FIELD_GROUPS, getFieldsByGroup } from '@/config/plan-fields' | 16 | import { PLAN_FIELD_DEFINITIONS, FIELD_GROUPS, getFieldsByGroup } from '@/config/plan-fields' |
| ... | @@ -90,6 +90,8 @@ describe('计划书模块集成测试', () => { | ... | @@ -90,6 +90,8 @@ describe('计划书模块集成测试', () => { |
| 90 | }) | 90 | }) |
| 91 | 91 | ||
| 92 | it('应该显示多文件选择弹框', async () => { | 92 | it('应该显示多文件选择弹框', async () => { |
| 93 | + viewAPI.mockResolvedValue({ code: 1 }) | ||
| 94 | + const { viewProposal: openProposal, actionSheetVisible, actionSheetMenuItems, handleChooseFile } = usePlanView() | ||
| 93 | const proposal = { | 95 | const proposal = { |
| 94 | id: 456, | 96 | id: 456, |
| 95 | order_status: '7', | 97 | order_status: '7', |
| ... | @@ -99,10 +101,17 @@ describe('计划书模块集成测试', () => { | ... | @@ -99,10 +101,17 @@ describe('计划书模块集成测试', () => { |
| 99 | ] | 101 | ] |
| 100 | } | 102 | } |
| 101 | 103 | ||
| 102 | - await viewProposal(proposal) | 104 | + await openProposal(proposal) |
| 105 | + | ||
| 106 | + expect(actionSheetVisible.value).toBe(true) | ||
| 107 | + expect(actionSheetMenuItems.value).toHaveLength(2) | ||
| 108 | + expect(actionSheetMenuItems.value[0].name).toBe('计划书A.pdf') | ||
| 109 | + | ||
| 110 | + await handleChooseFile(actionSheetMenuItems.value[1], 1) | ||
| 103 | 111 | ||
| 104 | - // 验证:显示选择弹框(Taro.showActionSheet) | 112 | + expect(actionSheetVisible.value).toBe(false) |
| 105 | - expect(Taro.showActionSheet).toHaveBeenCalled() | 113 | + expect(Taro.showActionSheet).not.toHaveBeenCalled() |
| 114 | + expect(viewAPI).toHaveBeenCalledWith({ i: 456 }) | ||
| 106 | }) | 115 | }) |
| 107 | 116 | ||
| 108 | it('应该在计划书未生成时友好提示', async () => { | 117 | it('应该在计划书未生成时友好提示', async () => { | ... | ... |
| ... | @@ -12,11 +12,18 @@ | ... | @@ -12,11 +12,18 @@ |
| 12 | */ | 12 | */ |
| 13 | 13 | ||
| 14 | import Taro from '@tarojs/taro' | 14 | import Taro from '@tarojs/taro' |
| 15 | +import { ref } from 'vue' | ||
| 15 | import { mapOrderStatus, getStatusText } from '@/config/constants/orderStatus' | 16 | import { mapOrderStatus, getStatusText } from '@/config/constants/orderStatus' |
| 16 | import { viewAPI } from '@/api/plan' | 17 | import { viewAPI } from '@/api/plan' |
| 17 | import { useFileOperation } from './useFileOperation' | 18 | import { useFileOperation } from './useFileOperation' |
| 18 | import { canViewProposal, getProposalFiles } from '@/utils/proposalView' | 19 | import { canViewProposal, getProposalFiles } from '@/utils/proposalView' |
| 19 | 20 | ||
| 21 | +const actionSheetVisible = ref(false) | ||
| 22 | +const actionSheetMenuItems = ref([]) | ||
| 23 | +let currentProposal = null | ||
| 24 | +let currentEmitError = null | ||
| 25 | +let currentOnViewSuccess = null | ||
| 26 | + | ||
| 20 | export const viewProposal = async (proposal, callbacks = {}) => { | 27 | export const viewProposal = async (proposal, callbacks = {}) => { |
| 21 | const { beforeView, onViewSuccess, onViewError, onError } = callbacks | 28 | const { beforeView, onViewSuccess, onViewError, onError } = callbacks |
| 22 | const emitError = (error) => { | 29 | const emitError = (error) => { |
| ... | @@ -83,24 +90,18 @@ export const viewProposal = async (proposal, callbacks = {}) => { | ... | @@ -83,24 +90,18 @@ export const viewProposal = async (proposal, callbacks = {}) => { |
| 83 | } | 90 | } |
| 84 | 91 | ||
| 85 | const fileList = proposalFiles.map((file, index) => ({ | 92 | const fileList = proposalFiles.map((file, index) => ({ |
| 86 | - text: file.file_name || `计划书 ${index + 1}`, | 93 | + name: file.file_name || `计划书 ${index + 1}`, |
| 87 | - file | 94 | + subname: `文件 ${index + 1}`, |
| 95 | + file, | ||
| 96 | + proposalId: proposal.id, | ||
| 97 | + index | ||
| 88 | })) | 98 | })) |
| 89 | 99 | ||
| 90 | - Taro.showActionSheet({ | 100 | + currentProposal = proposal |
| 91 | - itemList: fileList.map(item => item.text), | 101 | + currentEmitError = emitError |
| 92 | - success: async (res) => { | 102 | + currentOnViewSuccess = onViewSuccess |
| 93 | - if (res.tapIndex === undefined || res.tapIndex === null) return | 103 | + actionSheetMenuItems.value = fileList |
| 94 | - | 104 | + actionSheetVisible.value = true |
| 95 | - const selectedFile = fileList[res.tapIndex]?.file | ||
| 96 | - if (!selectedFile) return | ||
| 97 | - | ||
| 98 | - const previewSuccess = await handleFileView(selectedFile, emitError) | ||
| 99 | - if (previewSuccess) { | ||
| 100 | - await markViewed(proposal, onViewSuccess) | ||
| 101 | - } | ||
| 102 | - } | ||
| 103 | - }) | ||
| 104 | } catch (error) { | 105 | } catch (error) { |
| 105 | const errorMessage = error?.message || '查看计划书失败,请重试' | 106 | const errorMessage = error?.message || '查看计划书失败,请重试' |
| 106 | Taro.showToast({ | 107 | Taro.showToast({ |
| ... | @@ -111,6 +112,28 @@ export const viewProposal = async (proposal, callbacks = {}) => { | ... | @@ -111,6 +112,28 @@ export const viewProposal = async (proposal, callbacks = {}) => { |
| 111 | } | 112 | } |
| 112 | } | 113 | } |
| 113 | 114 | ||
| 115 | +export const closeActionSheet = () => { | ||
| 116 | + actionSheetVisible.value = false | ||
| 117 | + actionSheetMenuItems.value = [] | ||
| 118 | + currentProposal = null | ||
| 119 | + currentEmitError = null | ||
| 120 | + currentOnViewSuccess = null | ||
| 121 | +} | ||
| 122 | + | ||
| 123 | +export const handleChooseFile = async (item) => { | ||
| 124 | + const selectedFile = item?.file | ||
| 125 | + if (!selectedFile || !currentProposal) { | ||
| 126 | + closeActionSheet() | ||
| 127 | + return | ||
| 128 | + } | ||
| 129 | + | ||
| 130 | + const previewSuccess = await handleFileView(selectedFile, currentEmitError || (() => {})) | ||
| 131 | + if (previewSuccess) { | ||
| 132 | + await markViewed(currentProposal, currentOnViewSuccess) | ||
| 133 | + } | ||
| 134 | + closeActionSheet() | ||
| 135 | +} | ||
| 136 | + | ||
| 114 | const handleFileView = async (file, emitError) => { | 137 | const handleFileView = async (file, emitError) => { |
| 115 | if (!file?.file_url) { | 138 | if (!file?.file_url) { |
| 116 | const errorMsg = '文件链接无效' | 139 | const errorMsg = '文件链接无效' |
| ... | @@ -173,5 +196,9 @@ const markViewed = async (proposal, onViewSuccess) => { | ... | @@ -173,5 +196,9 @@ const markViewed = async (proposal, onViewSuccess) => { |
| 173 | } | 196 | } |
| 174 | 197 | ||
| 175 | export const usePlanView = () => ({ | 198 | export const usePlanView = () => ({ |
| 176 | - viewProposal | 199 | + viewProposal, |
| 200 | + actionSheetVisible, | ||
| 201 | + actionSheetMenuItems, | ||
| 202 | + handleChooseFile, | ||
| 203 | + closeActionSheet | ||
| 177 | }) | 204 | }) | ... | ... |
| ... | @@ -96,6 +96,8 @@ | ... | @@ -96,6 +96,8 @@ |
| 96 | 96 | ||
| 97 | <!-- 错误/空状态 --> | 97 | <!-- 错误/空状态 --> |
| 98 | <nut-empty v-else description="未找到消息内容" image="error" /> | 98 | <nut-empty v-else description="未找到消息内容" image="error" /> |
| 99 | + | ||
| 100 | + <ProposalFileActionSheet /> | ||
| 99 | </view> | 101 | </view> |
| 100 | </template> | 102 | </template> |
| 101 | 103 | ||
| ... | @@ -103,6 +105,7 @@ | ... | @@ -103,6 +105,7 @@ |
| 103 | import { ref, computed } from 'vue' | 105 | import { ref, computed } from 'vue' |
| 104 | import { useLoad } from '@tarojs/taro' | 106 | import { useLoad } from '@tarojs/taro' |
| 105 | import NavHeader from '@/components/navigation/NavHeader.vue' | 107 | import NavHeader from '@/components/navigation/NavHeader.vue' |
| 108 | +import ProposalFileActionSheet from '@/components/plan/ProposalFileActionSheet.vue' | ||
| 106 | import { detailAPI } from '@/api/news' | 109 | import { detailAPI } from '@/api/news' |
| 107 | import { useUserStore } from '@/stores/user' | 110 | import { useUserStore } from '@/stores/user' |
| 108 | import { mockDetailAPI } from '@/utils/mockData' | 111 | import { mockDetailAPI } from '@/utils/mockData' | ... | ... |
| ... | @@ -129,6 +129,7 @@ | ... | @@ -129,6 +129,7 @@ |
| 129 | 129 | ||
| 130 | <!-- TabBar --> | 130 | <!-- TabBar --> |
| 131 | <!-- <TabBar current="" /> --> | 131 | <!-- <TabBar current="" /> --> |
| 132 | + <ProposalFileActionSheet /> | ||
| 132 | </view> | 133 | </view> |
| 133 | </template> | 134 | </template> |
| 134 | 135 | ||
| ... | @@ -140,6 +141,7 @@ import { mockPlanListAPI } from '@/utils/mockData' | ... | @@ -140,6 +141,7 @@ import { mockPlanListAPI } from '@/utils/mockData' |
| 140 | import NavHeader from '@/components/navigation/NavHeader.vue' | 141 | import NavHeader from '@/components/navigation/NavHeader.vue' |
| 141 | import ListItemActions from '@/components/list/ListItemActions/index.vue' | 142 | import ListItemActions from '@/components/list/ListItemActions/index.vue' |
| 142 | import SearchBar from '@/components/forms/SearchBar.vue' | 143 | import SearchBar from '@/components/forms/SearchBar.vue' |
| 144 | +import ProposalFileActionSheet from '@/components/plan/ProposalFileActionSheet.vue' | ||
| 143 | import { USE_MOCK_DATA } from '@/config/app' | 145 | import { USE_MOCK_DATA } from '@/config/app' |
| 144 | import { mapOrderStatus, getStatusText } from '@/config/constants/orderStatus' | 146 | import { mapOrderStatus, getStatusText } from '@/config/constants/orderStatus' |
| 145 | import { canViewProposal as canViewProposalRule } from '@/utils/proposalView' | 147 | import { canViewProposal as canViewProposalRule } from '@/utils/proposalView' | ... | ... |
-
Please register or login to post a comment