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' {
MaterialCard: typeof import('./src/components/cards/MaterialCard.vue')['default']
NameInput: typeof import('./src/components/plan/PlanFields/NameInput.vue')['default']
NavHeader: typeof import('./src/components/navigation/NavHeader.vue')['default']
NutActionSheet: typeof import('@nutui/nutui-taro')['ActionSheet']
NutAvatar: typeof import('@nutui/nutui-taro')['Avatar']
NutButton: typeof import('@nutui/nutui-taro')['Button']
NutDatePicker: typeof import('@nutui/nutui-taro')['DatePicker']
......@@ -40,6 +41,7 @@ declare module 'vue' {
PlanFormContainer: typeof import('./src/components/plan/PlanFormContainer.vue')['default']
PlanPopupNew: typeof import('./src/components/plan/PlanPopupNew.vue')['default']
ProductCard: typeof import('./src/components/cards/ProductCard.vue')['default']
ProposalFileActionSheet: typeof import('./src/components/plan/ProposalFileActionSheet.vue')['default']
RadioGroup: typeof import('./src/components/plan/PlanFields/RadioGroup.vue')['default']
RichTextRenderer: typeof import('./src/components/RichTextRenderer.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
......
<template>
<nut-action-sheet
v-model:visible="actionSheetVisible"
:title="sheetTitle"
cancel-txt="取消"
:menu-items="actionSheetMenuItems"
:close-on-click-overlay="true"
@choose="handleChooseFile"
@cancel="closeActionSheet"
@close="closeActionSheet"
/>
</template>
<script setup>
/**
* 计划书文件选择动作面板
*
* @description 多文件计划书统一使用 NutUI ActionSheet 展示,
* 以支持长文件名换行和后续样式扩展。
*/
import { computed } from 'vue'
import { usePlanView } from '@/composables/usePlanView'
const { actionSheetVisible, actionSheetMenuItems, handleChooseFile, closeActionSheet } = usePlanView()
const sheetTitle = computed(() => {
const count = actionSheetMenuItems.value.length
return count > 0 ? `选择计划书文件 (${count}个)` : '选择计划书文件'
})
</script>
<style lang="less" scoped>
:deep(.nut-action-sheet__item) {
padding: 24rpx 32rpx;
text-align: left;
white-space: normal;
word-break: break-word;
}
:deep(.nut-action-sheet__item > view:first-child) {
font-weight: 500;
color: #1f2937;
white-space: normal;
word-break: break-word;
line-height: 1.5;
}
:deep(.nut-action-sheet__subdesc) {
margin-top: 8rpx;
line-height: 1.5;
color: #6b7280;
word-break: break-word;
}
</style>
......@@ -10,7 +10,7 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { ref, reactive } from 'vue'
import Taro from '@tarojs/taro'
import { viewProposal } from '../usePlanView'
import { usePlanView, viewProposal } from '../usePlanView'
import { useFieldValueTransform } from '../useFieldValueTransform'
import { useFieldDependencies } from '../useFieldDependencies'
import { PLAN_FIELD_DEFINITIONS, FIELD_GROUPS, getFieldsByGroup } from '@/config/plan-fields'
......@@ -90,6 +90,8 @@ describe('计划书模块集成测试', () => {
})
it('应该显示多文件选择弹框', async () => {
viewAPI.mockResolvedValue({ code: 1 })
const { viewProposal: openProposal, actionSheetVisible, actionSheetMenuItems, handleChooseFile } = usePlanView()
const proposal = {
id: 456,
order_status: '7',
......@@ -99,10 +101,17 @@ describe('计划书模块集成测试', () => {
]
}
await viewProposal(proposal)
await openProposal(proposal)
expect(actionSheetVisible.value).toBe(true)
expect(actionSheetMenuItems.value).toHaveLength(2)
expect(actionSheetMenuItems.value[0].name).toBe('计划书A.pdf')
await handleChooseFile(actionSheetMenuItems.value[1], 1)
// 验证:显示选择弹框(Taro.showActionSheet)
expect(Taro.showActionSheet).toHaveBeenCalled()
expect(actionSheetVisible.value).toBe(false)
expect(Taro.showActionSheet).not.toHaveBeenCalled()
expect(viewAPI).toHaveBeenCalledWith({ i: 456 })
})
it('应该在计划书未生成时友好提示', async () => {
......
......@@ -12,11 +12,18 @@
*/
import Taro from '@tarojs/taro'
import { ref } from 'vue'
import { mapOrderStatus, getStatusText } from '@/config/constants/orderStatus'
import { viewAPI } from '@/api/plan'
import { useFileOperation } from './useFileOperation'
import { canViewProposal, getProposalFiles } from '@/utils/proposalView'
const actionSheetVisible = ref(false)
const actionSheetMenuItems = ref([])
let currentProposal = null
let currentEmitError = null
let currentOnViewSuccess = null
export const viewProposal = async (proposal, callbacks = {}) => {
const { beforeView, onViewSuccess, onViewError, onError } = callbacks
const emitError = (error) => {
......@@ -83,24 +90,18 @@ export const viewProposal = async (proposal, callbacks = {}) => {
}
const fileList = proposalFiles.map((file, index) => ({
text: file.file_name || `计划书 ${index + 1}`,
file
name: file.file_name || `计划书 ${index + 1}`,
subname: `文件 ${index + 1}`,
file,
proposalId: proposal.id,
index
}))
Taro.showActionSheet({
itemList: fileList.map(item => item.text),
success: async (res) => {
if (res.tapIndex === undefined || res.tapIndex === null) return
const selectedFile = fileList[res.tapIndex]?.file
if (!selectedFile) return
const previewSuccess = await handleFileView(selectedFile, emitError)
if (previewSuccess) {
await markViewed(proposal, onViewSuccess)
}
}
})
currentProposal = proposal
currentEmitError = emitError
currentOnViewSuccess = onViewSuccess
actionSheetMenuItems.value = fileList
actionSheetVisible.value = true
} catch (error) {
const errorMessage = error?.message || '查看计划书失败,请重试'
Taro.showToast({
......@@ -111,6 +112,28 @@ export const viewProposal = async (proposal, callbacks = {}) => {
}
}
export const closeActionSheet = () => {
actionSheetVisible.value = false
actionSheetMenuItems.value = []
currentProposal = null
currentEmitError = null
currentOnViewSuccess = null
}
export const handleChooseFile = async (item) => {
const selectedFile = item?.file
if (!selectedFile || !currentProposal) {
closeActionSheet()
return
}
const previewSuccess = await handleFileView(selectedFile, currentEmitError || (() => {}))
if (previewSuccess) {
await markViewed(currentProposal, currentOnViewSuccess)
}
closeActionSheet()
}
const handleFileView = async (file, emitError) => {
if (!file?.file_url) {
const errorMsg = '文件链接无效'
......@@ -173,5 +196,9 @@ const markViewed = async (proposal, onViewSuccess) => {
}
export const usePlanView = () => ({
viewProposal
viewProposal,
actionSheetVisible,
actionSheetMenuItems,
handleChooseFile,
closeActionSheet
})
......
......@@ -96,6 +96,8 @@
<!-- 错误/空状态 -->
<nut-empty v-else description="未找到消息内容" image="error" />
<ProposalFileActionSheet />
</view>
</template>
......@@ -103,6 +105,7 @@
import { ref, computed } from 'vue'
import { useLoad } from '@tarojs/taro'
import NavHeader from '@/components/navigation/NavHeader.vue'
import ProposalFileActionSheet from '@/components/plan/ProposalFileActionSheet.vue'
import { detailAPI } from '@/api/news'
import { useUserStore } from '@/stores/user'
import { mockDetailAPI } from '@/utils/mockData'
......
......@@ -129,6 +129,7 @@
<!-- TabBar -->
<!-- <TabBar current="" /> -->
<ProposalFileActionSheet />
</view>
</template>
......@@ -140,6 +141,7 @@ import { mockPlanListAPI } from '@/utils/mockData'
import NavHeader from '@/components/navigation/NavHeader.vue'
import ListItemActions from '@/components/list/ListItemActions/index.vue'
import SearchBar from '@/components/forms/SearchBar.vue'
import ProposalFileActionSheet from '@/components/plan/ProposalFileActionSheet.vue'
import { USE_MOCK_DATA } from '@/config/app'
import { mapOrderStatus, getStatusText } from '@/config/constants/orderStatus'
import { canViewProposal as canViewProposalRule } from '@/utils/proposalView'
......