You need to sign in or sign up before continuing.
hookehuyr

refactor(plan): 替换 Taro.showActionSheet 为 NutUI ActionSheet 组件

将计划书多文件选择从原生 Taro.showActionSheet 迁移至
ProposalFileActionSheet 组件,支持长文件名换行显示和后续
样式扩展。usePlanView 暴露响应式 actionSheetVisible 等状态
供组件绑定,plan 与 message-detail 页面各挂载一份实例。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
...@@ -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'
......