src/pages.json | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/pages/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/pages/sales/invoiceLedger/detail.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/pages/sales/invoiceLedger/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/pages/sales/invoicingRegistration/add.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/pages/sales/receiptPayment/add.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/pages/sales/receiptPayment/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/pages/sales/salesAccount/detail.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/store/modules/user.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
src/pages.json
@@ -92,6 +92,34 @@ } }, { "path": "pages/sales/invoiceLedger/index", "style": { "navigationBarTitleText": "å¼ç¥¨å°è´¦", "navigationStyle": "custom" } }, { "path": "pages/sales/invoiceLedger/detail", "style": { "navigationBarTitleText": "ç¼è¾å¼ç¥¨å°è´¦", "navigationStyle": "custom" } }, { "path": "pages/sales/receiptPayment/index", "style": { "navigationBarTitleText": "忬¾ç»è®°", "navigationStyle": "custom" } }, { "path": "pages/sales/receiptPayment/add", "style": { "navigationBarTitleText": "æ°å¢å款", "navigationStyle": "custom" } }, { "path": "pages/common/webview/index", "style": { "navigationBarTitleText": "æµè§ç½é¡µ" src/pages/index.vue
@@ -298,6 +298,16 @@ url: '/pages/sales/invoicingRegistration/index' }); break; case 'å¼ç¥¨å°è´¦': uni.navigateTo({ url: '/pages/sales/invoiceLedger/index' }); break; case '忬¾ç»è®°': uni.navigateTo({ url: '/pages/sales/receiptPayment/index' }); break; case 'åå审æ¹': uni.navigateTo({ url: '/pages/cooperativeOffice/collaborativeApproval/index' src/pages/sales/invoiceLedger/detail.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,277 @@ <template> <view class="account-detail"> <van-nav-bar title="ç¼è¾å¼ç¥¨å°è´¦" left-text="è¿å" left-arrow @click-left="goBack" fixed placeholder /> <van-form @submit="submitForm" ref="formRef" label-width="110px" input-align="right" error-message-align="right" scroll-to-error scroll-to-error-position="center"> <van-cell-group title="åºæ¬ä¿¡æ¯" inset> <van-field v-model="form.salesContractNo" label="éå®ååå·" readonly /> <van-field v-model="form.customerName" label="客æ·åç§°" readonly /> <van-field v-model="form.invoiceNo" label="å票å·" placeholder="请è¾å ¥" required :rules="[{ required: true, message: '请è¾å ¥å票å·' }]" /> <van-field v-model="form.invoiceTotal" label="å票éé¢(å )" type="number" placeholder="请è¾å ¥" required :rules="[{ required: true, message: '请è¾å ¥å票éé¢' }]" /> <view class="tip-text" v-if="form.taxInclusiveTotalPrice">ååæ»é¢ï¼{{ formatAmount(form.taxInclusiveTotalPrice) }} å </view> <van-field v-model="form.invoicePerson" label="å¼ç¥¨äºº" readonly /> <van-field v-model="form.invoiceDate" label="å¼ç¥¨æ¥æ" readonly placeholder="è¯·éæ©" @click="showInvoiceDatePicker = true" required :rules="[{ required: true, message: 'è¯·éæ©å¼ç¥¨æ¥æ' }]" /> </van-cell-group> <van-cell-group title="éä»¶ææï¼ä» æ¯æ pdfï¼" inset> <van-uploader accept=".pdf" multiple :after-read="afterReadUpload" :before-read="beforeReadPdf" > <van-button class="upload-btn" icon="plus" type="primary" block>ä¸ä¼ æä»¶</van-button> </van-uploader> <view class="uploaded-list" v-if="fileList.length"> <view class="uploaded-item" v-for="(f, idx) in fileList" :key="idx"> <text class="file-name">{{ f.name || getFileNameFromUrl(f.url) }}</text> <van-button size="mini" type="danger" plain @click="removeUploaded(idx)">ç§»é¤</van-button> </view> </view> </van-cell-group> <view class="footer-btns"> <van-button class="cancel-btn" @click="goBack">åæ¶</van-button> <van-button class="save-btn" native-type="submit" form-type="submit">ä¿å</van-button> </view> </van-form> <van-popup v-model:show="showInvoiceDatePicker" position="bottom"> <van-date-picker v-model="currentInvoiceDate" title="éæ©å¼ç¥¨æ¥æ" @confirm="onInvoiceDateConfirm" @cancel="showInvoiceDatePicker = false" /> </van-popup> </view> </template> <script setup> import { ref, onMounted } from 'vue' import { showToast, showLoadingToast, closeToast } from 'vant' import dayjs from 'dayjs' import useUserStore from '@/store/modules/user' import { getToken } from '@/utils/auth' import { invoiceLedgerProductInfo, invoiceLedgerSaveOrUpdate } from '@/api/salesManagement/invoiceLedger.js' import config from '@/config.js' const userStore = useUserStore() const formRef = ref() let form = ref({ salesLedgerId: '', customerId: '', invoiceNo: '', invoiceTotal: '', taxRate: '', invoicePerson: '', invoiceDate: '', customerName: '', fileList: [], createTime: '', taxInclusiveTotalPrice: '' }) const fileList = ref([]) const currentId = ref('') // æ¥æéæ© const showInvoiceDatePicker = ref(false) const currentInvoiceDate = ref([new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()]) const goBack = () => { uni.removeStorageSync('invoiceLedgerEditRow'); uni.navigateBack() } const formatAmount = (val) => { if (val === undefined || val === null || val === '') return '0.00' const num = Number(val) if (Number.isNaN(num)) return '0.00' return num.toFixed(2) } const onInvoiceDateConfirm = ({ selectedValues }) => { form.value.invoiceDate = selectedValues.join('-') currentInvoiceDate.value = selectedValues showInvoiceDatePicker.value = false } // ä¸ä¼ åæ ¡éªï¼å ¼å®¹ Vant Uploader ç file/fileList ç»æï¼ const beforeReadPdf = (file) => { const items = Array.isArray(file) ? file : [file] for (const it of items) { const raw = it?.file || it const fileName = raw?.name || it?.name || '' const ext = fileName.split('.').pop()?.toLowerCase() const sizeOk = (raw?.size || 0) <= 10 * 1024 * 1024 if (ext !== 'pdf') { showToast('ä» æ¯æpdfæä»¶') return false } if (!sizeOk) { showToast('ä¸ä¼ æä»¶å¤§å°ä¸è½è¶ è¿10MB') return false } } return true } const uploadSingleFile = async (fileObj) => { return new Promise((resolve, reject) => { showLoadingToast({ message: 'æ£å¨ä¸ä¼ ...' }) const baseUrl = config.baseUrl + '/invoiceLedger/uploadFile' const filePath = fileObj?.url || fileObj?.tempFilePath || fileObj?.file?.path if (filePath) { uni.uploadFile({ url: baseUrl, filePath, name: 'file', header: { Authorization: 'Bearer ' + getToken() }, success: (res) => { closeToast() try { const data = JSON.parse(res.data || '{}') if (data.code === 200) { resolve(data.data) } else { reject(new Error(data.msg || 'ä¸ä¼ 失败')) } } catch (err) { reject(err) } }, fail: (err) => { closeToast() reject(err) } }) return } // H5: 使ç¨åå§ Fileï¼input éæ©ï¼ const rawFile = fileObj?.file if (rawFile) { // uni.uploadFile å¨ H5 䏿¯æåç File 对象ï¼è¿éç¨ fetch åé FormData const formData = new FormData() formData.append('file', rawFile, rawFile.name || 'file.pdf') formData.append('salesLedgerId', form.value.salesLedgerId || currentId.value || '') fetch(baseUrl, { method: 'POST', headers: { Authorization: 'Bearer ' + getToken() }, body: formData }).then(async (res) => { closeToast() const data = await res.json() if (data.code === 200) { resolve(data.data) } else { reject(new Error(data.msg || 'ä¸ä¼ 失败')) } }).catch((err) => { closeToast() reject(err) }) return } closeToast() reject(new Error('æªæ¾å°å¯ä¸ä¼ çæä»¶')) }) } const afterReadUpload = async (file) => { try { const files = Array.isArray(file) ? file : file?.file ? [file] : [file] for (const f of files) { const uploaded = await uploadSingleFile(f) fileList.value.push(uploaded) } showToast('ä¸ä¼ æå') } catch (e) { showToast('ä¸ä¼ 失败') } } const removeUploaded = (index) => { fileList.value.splice(index, 1) } const getFileNameFromUrl = (url) => { try { if (!url) return ''; return decodeURIComponent(url.split('/').pop()) } catch (e) { return url } } const loadDetail = async (id) => { try { showLoadingToast({ message: 'å è½½ä¸...' }) const res = await invoiceLedgerProductInfo({ id }) const data = res?.data || res form.value = { ...data } fileList.value = data?.fileList || [] if (!form.value.invoicePerson) { form.value.invoicePerson = userStore.nickName } if (!form.value.invoiceDate) { form.value.invoiceDate = dayjs().format('YYYY-MM-DD') } closeToast() } catch (e) { closeToast() showToast('å 载失败') } } const submitForm = async () => { try { if (!form.value.invoiceNo) { showToast('请è¾å ¥å票å·'); return } if (!form.value.invoiceTotal) { showToast('请è¾å ¥å票éé¢'); return } if (!form.value.invoiceDate) { showToast('è¯·éæ©å¼ç¥¨æ¥æ'); return } showLoadingToast({ message: 'æäº¤ä¸...' }) form.value.fileList = fileList.value await invoiceLedgerSaveOrUpdate(form.value) closeToast() showToast('æäº¤æå') setTimeout(() => { uni.navigateBack() }, 800) } catch (e) { closeToast() showToast('æäº¤å¤±è´¥ï¼è¯·éè¯') } } onMounted(() => { const rowStr = uni.getStorageSync('invoiceLedgerEditRow') if (rowStr) { try { const row = JSON.parse(rowStr) currentId.value = row.id loadDetail(currentId.value) } catch (e) { // ignore } } }) </script> <style scoped lang="scss"> .account-detail { min-height: 100vh; background: #f8f9fa; padding-bottom: 5rem; } .uploaded-list { padding: 8px 16px 0 16px; } .uploaded-item { display: flex; align-items: center; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #f5f5f5; } .file-name { font-size: 12px; color: #333; margin-right: 8px; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .tip-text { padding: 4px 16px 0 16px; font-size: 12px; color: #888; } .footer-btns { position: fixed; left: 0; right: 0; bottom: 0; background: #fff; display: flex; justify-content: space-around; align-items: center; padding: 0.75rem 0; box-shadow: 0 -0.125rem 0.5rem rgba(0,0,0,0.05); z-index: 1000; } .cancel-btn { font-weight: 400; font-size: 1rem; color: #FFFFFF; width: 6.375rem; background: #C7C9CC; box-shadow: 0 0.25rem 0.625rem 0 rgba(3,88,185,0.2); border-radius: 2.5rem 2.5rem 2.5rem 2.5rem; } .save-btn { font-weight: 400; font-size: 1rem; color: #FFFFFF; width: 14rem; background: linear-gradient( 140deg, #00BAFF 0%, #006CFB 100%); box-shadow: 0 0.25rem 0.625rem 0 rgba(3,88,185,0.2); border-radius: 2.5rem 2.5rem 2.5rem 2.5rem; } </style> src/pages/sales/invoiceLedger/index.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,759 @@ <template> <view class="sales-account"> <!-- 页é¢å¤´é¨ --> <van-nav-bar title="å¼ç¥¨å°è´¦" left-text="è¿å" left-arrow @click-left="goBack" fixed placeholder /> <!-- æç´¢åçéåºåï¼ä¿æä¸éå®å°è´¦é£æ ¼ä¸è´ï¼ --> <view class="search-filter-section"> <view class="search-bar"> <view class="search-input"> <input class="search-text" placeholder="客æ·åç§°/éå®ååå·" v-model="searchForm.searchText" confirm-type="search" @confirm="handleQuery" /> </view> <!-- <view class="filter-button" @click="showFilter = true">--> <!-- <up-icon name="list" size="24" color="#999"></up-icon>--> <!-- </view>--> <view class="filter-button" @click="handleQuery"> <up-icon name="search" size="24" color="#999"></up-icon> </view> </view> </view> <!-- å表åºå --> <view class="ledger-list" v-if="total > 0"> <view v-for="(item, index) in ledgerList" :key="index"> <view class="ledger-item"> <view class="item-header"> <view class="item-left"> <view class="document-icon"> <up-icon name="file-text" size="16" color="#ffffff"></up-icon> </view> <text class="item-id">{{ item.salesContractNo }}</text> </view> </view> <up-divider></up-divider> <view class="item-details"> <view class="detail-row"> <text class="detail-label">客æ·åç§°</text> <text class="detail-value">{{ item.customerName }}</text> </view> <view class="detail-row"> <text class="detail-label">客æ·ååå·</text> <text class="detail-value">{{ item.customerContractNo }}</text> </view> <view class="detail-row"> <text class="detail-label">项ç®</text> <text class="detail-value">{{ item.projectName }}</text> </view> <view class="detail-row"> <text class="detail-label">产å大类</text> <text class="detail-value">{{ item.productCategory }}</text> </view> <view class="detail-row"> <text class="detail-label">è§æ ¼åå·</text> <text class="detail-value">{{ item.specificationModel }}</text> </view> <view class="detail-row"> <text class="detail-label">å票å·</text> <text class="detail-value">{{ item.invoiceNo || '-' }}</text> </view> <view class="detail-row"> <text class="detail-label">å票éé¢(å )</text> <text class="detail-value highlight">{{ formatAmount(item.invoiceTotal) }}</text> </view> <view class="detail-row"> <text class="detail-label">ç¨ç(%)</text> <text class="detail-value">{{ item.taxRate }}</text> </view> <view class="detail-row"> <text class="detail-label">å½å ¥äºº</text> <text class="detail-value">{{ item.invoicePerson }}</text> </view> <view class="detail-row"> <text class="detail-label">å½å ¥æ¥æ</text> <text class="detail-value">{{ formatDateTime(item.createTime) }}</text> </view> <view class="detail-row"> <text class="detail-label">å¼ç¥¨æ¥æ</text> <text class="detail-value">{{ item.invoiceDate || '-' }}</text> </view> </view> <view class="action-buttons"> <van-button type="primary" size="small" class="action-btn" :disabled="item.invoicePerson !== userStore.nickName" @click="openEdit(item)" > ç¼è¾ </van-button> <van-button type="danger" size="small" class="action-btn" :disabled="item.invoicePerson !== userStore.nickName" @click="handleDelete(item)" > å é¤ </van-button> <van-button type="default" size="small" class="action-btn" v-if="item.invoiceFileName" @click="openFileActions(item.commonFiles || [])" > æ¥çéä»¶ </van-button> <van-button type="primary" size="small" plain class="action-btn" v-else :disabled="item.invoicePerson !== userStore.nickName" @click="openUpload(item)" > ä¸ä¼ </van-button> </view> </view> </view> </view> <view v-else class="no-data"> <text>ææ å¼ç¥¨å°è´¦æ°æ®</text> </view> <!-- çéå¼¹çª --> <van-popup v-model:show="showFilter" position="bottom" round> <view class="filter-popup"> <van-cell-group title="ç鿡件" inset> <van-field label="å¼ç¥¨æ¥æ" readonly @click="showInvoiceRange = true" :placeholder="invoiceRangeLabel || 'è¯·éæ©æ¥æèå´'" /> <van-field label="å½å ¥æ¥æ" readonly @click="showCreateDatePicker = true" :placeholder="searchForm.createTimeStart || 'è¯·éæ©å½å ¥æ¥æ'" /> <view class="switch-row"> <text class="switch-label">䏿¾ç¤ºæå票è¡</text> <van-switch v-model="searchForm.status" size="20" /> </view> </van-cell-group> <view class="filter-actions"> <van-button @click="resetFilter">éç½®</van-button> <van-button type="primary" @click="confirmFilter">ç¡®å®</van-button> </view> </view> </van-popup> <!-- æ¥åï¼å¼ç¥¨æ¥æèå´ --> <van-popup v-model:show="showInvoiceRange" position="bottom"> <van-calendar title="éæ©å¼ç¥¨æ¥æèå´" type="range" color="#2979ff" @confirm="onInvoiceRangeConfirm" @cancel="showInvoiceRange = false" /> </van-popup> <!-- æ¥æï¼å½å ¥æ¥æ --> <van-popup v-model:show="showCreateDatePicker" position="bottom"> <van-date-picker v-model="currentCreateDate" title="éæ©å½å ¥æ¥æ" @confirm="onCreateDateConfirm" @cancel="showCreateDatePicker = false" /> </van-popup> <!-- åè¡ä¸ä¼ å¼¹çªï¼æ 表åï¼ --> <van-popup v-model:show="showUpload" position="bottom" round> <view class="upload-container"> <van-cell-group title="ä¸ä¼ éä»¶ï¼ä» æ¯æ pdfï¼æå¤§10MBï¼æå¤10个ï¼" inset> <van-uploader accept="*" multiple :max-count="10" :after-read="afterReadRowUpload" :before-read="beforeReadPdf" /> <view class="uploaded-list" v-if="fileList.length"> <view class="uploaded-item" v-for="(f, idx) in fileList" :key="idx"> <text class="file-name">{{ f.name || getFileNameFromUrl(f.url) }}</text> <van-button size="mini" type="danger" plain @click="removeUploaded(idx)">ç§»é¤</van-button> </view> </view> </van-cell-group> <view class="filter-actions"> <van-button @click="showUpload = false">åæ¶</van-button> <van-button type="primary" @click="confirmUpload">确认</van-button> </view> </view> </van-popup> <!-- éä»¶åè¡¨éæ© --> <van-action-sheet v-model:show="showFileSheet" :actions="fileActions" cancel-text="åæ¶" close-on-click-action @select="onSelectFile" /> </view> </template> <script setup> import { ref, reactive, onMounted } from 'vue' import dayjs from 'dayjs' import { showToast, showLoadingToast, closeToast } from 'vant' import useUserStore from '@/store/modules/user' import { getToken } from '@/utils/auth' import config from '@/config.js' import { registrationProductPage, commitFile, delInvoiceLedgerByRegProductId } from '@/api/salesManagement/invoiceLedger.js' import {onShow} from "@dcloudio/uni-app"; const userStore = useUserStore() // åè¡¨ä¸æ¥è¯¢ const ledgerList = ref([]) const total = ref(0) const page = reactive({ current: -1, size: -1 }) const searchForm = reactive({ searchText: '', status: false, createTimeStart: '' }) // é¡¶é¨äº¤äº const showFilter = ref(false) const showInvoiceRange = ref(false) const showCreateDatePicker = ref(false) const invoiceRangeLabel = ref('') const currentCreateDate = ref([new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()]) const currentId = ref('') const fileList = ref([]) // è¡ä¸ä¼ æéç¨ä¸ä¼ å表 // è¡ä¸ä¼ å¼¹çª const showUpload = ref(false) // éä»¶æ¥ç const showFileSheet = ref(false) const fileActions = ref([]) let currentFilesToOpen = [] const formatAmount = (val) => { if (val === undefined || val === null || val === '') return '0.00' const num = Number(val) if (Number.isNaN(num)) return '0.00' return num.toFixed(2) } const formatDateTime = (val) => { if (!val) return '' return dayjs(val).format('YYYY-MM-DD HH:mm:ss') } const goBack = () => { uni.navigateBack() } const handleQuery = () => { getList() } const getList = async () => { try { showLoadingToast({ message: 'å è½½ä¸...' }) const { invoiceDate, ...rest } = searchForm const res = await registrationProductPage({ ...rest, ...page }) // å ¼å®¹ä¸åè¿åç»æ const records = res?.data?.records || res?.records || res?.data || [] const totalVal = res?.data?.total || res?.total || records.length || 0 ledgerList.value = records total.value = totalVal closeToast() } catch (e) { closeToast() showToast('è·åå表失败') } } // çéé»è¾ const resetFilter = () => { searchForm.searchText = '' searchForm.status = false const start = dayjs().startOf('month').format('YYYY-MM-DD') const end = dayjs().endOf('month').format('YYYY-MM-DD') searchForm.invoiceDate = [start, end] searchForm.invoiceDateStart = start searchForm.invoiceDateEnd = end searchForm.createTimeStart = '' invoiceRangeLabel.value = '' } const confirmFilter = () => { showFilter.value = false getList() } const onInvoiceRangeConfirm = (e) => { // e 为 [start, end] ç Date 对象æå符串ï¼uni-app ä¸ Vant Calendar è¿åæ¶é´æ³æ°ç» try { let start, end if (Array.isArray(e)) { const [s, ed] = e start = dayjs(s).format('YYYY-MM-DD') end = dayjs(ed).format('YYYY-MM-DD') } else if (e && e.detail && Array.isArray(e.detail)) { const [s, ed] = e.detail start = dayjs(s).format('YYYY-MM-DD') end = dayjs(ed).format('YYYY-MM-DD') } searchForm.invoiceDateStart = start searchForm.invoiceDateEnd = end invoiceRangeLabel.value = `${start} è³ ${end}` showInvoiceRange.value = false } catch (err) { showInvoiceRange.value = false } } const onCreateDateConfirm = ({ selectedValues }) => { try { searchForm.createTimeStart = selectedValues.join('-') currentCreateDate.value = selectedValues showCreateDatePicker.value = false } catch (err) { showCreateDatePicker.value = false } } // ç¼è¾é»è¾æ¹ä¸ºè·³è½¬æ°é¡µé¢ const openEdit = (row) => { try { uni.setStorageSync('invoiceLedgerEditRow', JSON.stringify(row)) uni.navigateTo({ url: '/pages/sales/invoiceLedger/detail' }) } catch (e) { showToast('跳转失败') } } // å é¤ const handleDelete = (row) => { uni.showModal({ title: 'å é¤ç¡®è®¤', content: '该å票å°è´¦å°è¢«å é¤ï¼æ¯å¦ç¡®è®¤å é¤ï¼', success: async (res) => { if (res.confirm) { try { showLoadingToast({ message: 'å¤çä¸...' }) await delInvoiceLedgerByRegProductId(row.id) closeToast() showToast('å 餿å') getList() } catch (e) { closeToast() showToast('å é¤å¤±è´¥ï¼è¯·éè¯') } } } }) } // è¡ä¸ä¼ const openUpload = (row) => { currentId.value = row.id fileList.value = [] showUpload.value = true } const confirmUpload = async () => { try { const payload = { fileList: fileList.value, id: currentId.value } showLoadingToast({ message: 'æäº¤ä¸...' }) await commitFile(payload) closeToast() showToast('æäº¤æå') showUpload.value = false fileList.value = [] currentId.value = '' getList() } catch (e) { closeToast() showToast('æäº¤å¤±è´¥ï¼è¯·éè¯') } } // ä¸ä¼ ç¸å ³ const beforeReadPdf = (file) => { // å ¼å®¹å¤æä»¶ const files = Array.isArray(file) ? file : [file] for (const f of files) { const sizeOk = f.size <= 10 * 1024 * 1024 const ext = (f.name || '').split('.').pop()?.toLowerCase() if (ext !== 'pdf') { showToast('ä» æ¯æpdfæä»¶') return false } if (!sizeOk) { showToast('ä¸ä¼ æä»¶å¤§å°ä¸è½è¶ è¿10MB') return false } } return true } const uploadSingleFile = async (fileObj) => { return new Promise((resolve, reject) => { showLoadingToast({ message: 'æ£å¨ä¸ä¼ ...' }) uni.uploadFile({ url: config.baseUrl + '/invoiceLedger/uploadFile', filePath: fileObj.url || fileObj.file?.path || fileObj.tempFilePath, name: 'file', header: { Authorization: 'Bearer ' + getToken() }, success: (res) => { closeToast() try { const data = JSON.parse(res.data || '{}') if (data.code === 200) { resolve(data.data) } else { reject(new Error(data.msg || 'ä¸ä¼ 失败')) } } catch (err) { reject(err) } }, fail: (err) => { closeToast() reject(err) } }) }) } const afterReadEditUpload = async (file) => { try { const files = Array.isArray(file) ? file : file?.file ? [file] : [file] for (const f of files) { const uploaded = await uploadSingleFile(f) fileList.value.push(uploaded) } showToast('ä¸ä¼ æå') } catch (e) { showToast('ä¸ä¼ 失败') } } const afterReadRowUpload = async (file) => { try { const files = Array.isArray(file) ? file : file?.file ? [file] : [file] for (const f of files) { const uploaded = await uploadSingleFile(f) fileList.value.push(uploaded) } showToast('ä¸ä¼ æå') } catch (e) { showToast('ä¸ä¼ 失败') } } const removeUploaded = (index) => { fileList.value.splice(index, 1) } const getFileNameFromUrl = (url) => { try { if (!url) return '' return decodeURIComponent(url.split('/').pop()) } catch (e) { return url } } // éä»¶æ¥ç const openFileActions = (commonFiles) => { currentFilesToOpen = commonFiles || [] fileActions.value = (commonFiles || []).map((f, idx) => ({ name: getFileNameFromUrl(f.url || ''), index: idx })) showFileSheet.value = true } const onSelectFile = async (action) => { try { const item = currentFilesToOpen[action.index] if (!item || !item.url) return showLoadingToast({ message: 'ä¸è½½ä¸...' }) uni.downloadFile({ url: item.url, success: (res) => { closeToast() if (res.statusCode === 200) { uni.openDocument({ filePath: res.tempFilePath }) } else { showToast('ä¸è½½å¤±è´¥') } }, fail: () => { closeToast() showToast('ä¸è½½å¤±è´¥') } }) } catch (e) { closeToast() showToast('æå¼å¤±è´¥') } } onShow(() => { getList() }) </script> <style scoped lang="scss"> .u-divider { margin: 0 !important; } .sales-account { min-height: 100vh; background: #f8f9fa; position: relative; } .search-filter-section { padding: 10px 20px; background: #ffffff; } .search-bar { display: flex; align-items: center; gap: 12px; } .search-input { flex: 1; background: #f5f5f5; border-radius: 24px; padding: 10px 16px; display: flex; align-items: center; gap: 8px; } .search-text { flex: 1; font-size: 14px; color: #333; background: transparent; border: none; outline: none; } .search-text::placeholder { color: #999; } .filter-button { width: 40px; height: 40px; border-radius: 8px; display: flex; align-items: center; justify-content: center; } .ledger-list { padding: 20px; } .ledger-item { background: #ffffff; border-radius: 12px; margin-bottom: 16px; overflow: hidden; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); padding: 0 16px; } .item-header { padding: 16px 0; display: flex; align-items: center; justify-content: space-between; } .item-left { display: flex; align-items: center; gap: 8px; } .document-icon { width: 24px; height: 24px; background: #2979ff; border-radius: 4px; display: flex; align-items: center; justify-content: center; } .item-id { font-size: 14px; color: #333; font-weight: 500; } .item-details { padding: 16px 0; } .detail-row { display: flex; align-items: flex-end; justify-content: space-between; margin-bottom: 8px; &:last-child { margin-bottom: 0; } } .detail-label { font-size: 12px; color: #777777; min-width: 60px; } .detail-value { font-size: 12px; color: #000000; text-align: right; flex: 1; margin-left: 16px; } .detail-value.highlight { color: #2979ff; font-weight: 500; } .no-data { padding: 40px 0; text-align: center; color: #999; } .action-buttons { display: flex; gap: 12px; padding: 0 0 16px 0; justify-content: space-between; } .action-btn { flex: 1; } .filter-popup { padding: 12px 12px 20px; } .switch-row { padding: 12px 16px; display: flex; align-items: center; justify-content: space-between; } .switch-label { font-size: 14px; color: #333; } .filter-actions { display: flex; gap: 12px; padding: 12px 16px 16px; justify-content: space-between; } .edit-container { padding-bottom: 5rem; } .uploaded-list { padding: 8px 16px 0 16px; } .uploaded-item { display: flex; align-items: center; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #f5f5f5; } .file-name { font-size: 12px; color: #333; margin-right: 8px; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .tip-text { padding: 4px 16px 0 16px; font-size: 12px; color: #888; } .footer-btns { position: fixed; left: 0; right: 0; bottom: 0; background: #fff; display: flex; justify-content: space-around; align-items: center; padding: 0.75rem 0; box-shadow: 0 -0.125rem 0.5rem rgba(0,0,0,0.05); z-index: 1000; } .cancel-btn { font-weight: 400; font-size: 1rem; color: #FFFFFF; width: 6.375rem; background: #C7C9CC; box-shadow: 0 0.25rem 0.625rem 0 rgba(3,88,185,0.2); border-radius: 2.5rem 2.5rem 2.5rem 2.5rem; } .save-btn { font-weight: 400; font-size: 1rem; color: #FFFFFF; width: 14rem; background: linear-gradient( 140deg, #00BAFF 0%, #006CFB 100%); box-shadow: 0 0.25rem 0.625rem 0 rgba(3,88,185,0.2); border-radius: 2.5rem 2.5rem 2.5rem 2.5rem; } </style> src/pages/sales/invoicingRegistration/add.vue
@@ -358,7 +358,7 @@ productData.value = res.productData; form.value = { ...res }; // 设置é»è®¤å½å ¥äºº form.value.createUer = userStore.name || '' form.value.createUer = userStore.nickName || '' // 设置é»è®¤æ¥æ const today = new Date() src/pages/sales/receiptPayment/add.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,299 @@ <template> <view class="account-detail"> <!-- 页é¢å¤´é¨ --> <van-nav-bar title="æ°å¢å款" left-text="è¿å" left-arrow @click-left="onClickLeft" fixed placeholder /> <!-- 表åå 容 --> <van-form @submit="onSubmit" ref="formRef" label-width="110px" input-align="right" error-message-align="right" scroll-to-error scroll-to-error-position="center"> <!-- åºæ¬ä¿¡æ¯ --> <van-cell-group title="åºæ¬ä¿¡æ¯" inset> <van-field v-model="form.salesContractNo" label="éå®ååå·" placeholder="èªå¨å¡«å " readonly /> <van-field v-model="form.customerName" label="客æ·åç§°" placeholder="èªå¨å¡«å " readonly /> <van-field v-model="form.invoiceNo" label="å票å·" placeholder="èªå¨å¡«å " readonly /> <van-field v-model="form.invoiceTotal" label="å票éé¢(å )" placeholder="èªå¨å¡«å " readonly /> <van-field v-model="form.taxRate" label="ç¨ç" placeholder="èªå¨å¡«å " readonly /> <view class="tip-text">å¾ åæ¬¾éé¢ï¼{{ currentNoReceiptAmount }} å </view> <van-field v-model="form.receiptPaymentAmount" label="æ¬æ¬¡å款éé¢" type="number" placeholder="请è¾å ¥" @blur="changeNum" :rules="[{ required: true, message: '请è¾å ¥å款éé¢' }]" clearable /> <van-field v-model="form.receiptPaymentTypeName" label="忬¾å½¢å¼" placeholder="è¯·éæ©" readonly @click="showPaymentTypePicker" :rules="[{ required: true, message: 'è¯·éæ©åæ¬¾å½¢å¼' }]" /> <van-field v-model="form.receiptPaymentDate" label="æ¥æ¬¾æ¥æ" placeholder="è¯·éæ©" readonly @click="showDatePicker" :rules="[{ required: true, message: 'è¯·éæ©æ¥æ¬¾æ¥æ' }]" /> <van-field v-model="form.registrant" label="ç»è®°äºº" placeholder="èªå¨å¡«å " readonly /> </van-cell-group> <!-- æäº¤æé® --> <view class="footer-btns"> <van-button class="cancel-btn" @click="onClickLeft">åæ¶</van-button> <van-button class="save-btn" native-type="submit" form-type="submit" :loading="loading">ä¿å</van-button> </view> </van-form> <!-- 忬¾æ¹å¼éæ©å¨ --> <van-popup v-model:show="showPaymentType" position="bottom"> <van-picker :model-value="pickerValue" :columns="receipt_payment_type" @confirm="onPaymentTypeConfirm" @cancel="showPaymentType = false" /> </van-popup> <!-- æ¥æéæ©å¨ --> <van-popup v-model:show="showDate" position="bottom"> <van-date-picker v-model="currentDate" title="éæ©æ¥æ" @confirm="onDateConfirm" @cancel="showDate = false" /> </van-popup> </view> </template> <script setup> import { ref, onMounted, computed } from 'vue' import { receiptPaymentSaveOrUpdate, invoiceInfo } from '@/api/salesManagement/receiptPayment' import useUserStore from '@/store/modules/user' import { showToast, showNotify } from 'vant' import { useDict } from '@/utils/dict' const userStore = useUserStore() // 表åå¼ç¨ const formRef = ref() // ååºå¼æ°æ® const loading = ref(false) const showPaymentType = ref(false) const pickerValue = ref([]) const showDate = ref(false) const currentDate = ref([new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()]) // è¡¨åæ°æ® const form = ref({ salesContractNo: '', customerName: '', invoiceNo: '', invoiceTotal: '', taxRate: '', receiptPaymentAmount: '', receiptPaymentType: '', receiptPaymentTypeName: '', registrant: '', receiptPaymentDate: '', invoiceLedgerId: '' }) const currentNoReceiptAmount = ref(0) // è·ååå ¸æ°æ® const { receipt_payment_type: dictReceiptPaymentType } = useDict('receipt_payment_type') // 转æ¢åå ¸æ°æ®æ ¼å¼ä¸ºéæ©å¨éè¦çæ ¼å¼ const receipt_payment_type = computed(() => { return dictReceiptPaymentType.value.map(item => ({ text: item.label, value: item.value })) }) // è¿åä¸ä¸é¡µ const onClickLeft = () => { uni.removeStorageSync('invoiceLedgerEditRow'); uni.navigateBack() } // æ¾ç¤ºå款æ¹å¼éæ©å¨ const showPaymentTypePicker = () => { showPaymentType.value = true } const changeNum = () => { if (form.value.receiptPaymentAmount > currentNoReceiptAmount.value) { form.value.receiptPaymentAmount = currentNoReceiptAmount.value showToast('ä¸å¯å¤§äºå¾ 忬¾éé¢') } } // ç¡®è®¤åæ¬¾æ¹å¼éæ© const onPaymentTypeConfirm = ({ selectedValues, selectedOptions }) => { form.value.receiptPaymentType = selectedOptions[0].value form.value.receiptPaymentTypeName = selectedOptions[0].text pickerValue.value = selectedValues; showPaymentType.value = false } // æ¾ç¤ºæ¥æéæ©å¨ const showDatePicker = () => { showDate.value = true } // ç¡®è®¤æ¥æéæ© const onDateConfirm = ({ selectedValues }) => { form.value.receiptPaymentDate = selectedValues.join('-') currentDate.value = selectedValues showDate.value = false } // æäº¤è¡¨å const onSubmit = () => { // 表åéªè¯ if (!form.value.receiptPaymentAmount) { showNotify({ type: 'warning', message: '请è¾å ¥å款éé¢' }) return } if (!form.value.receiptPaymentType) { showNotify({ type: 'warning', message: 'è¯·éæ©åæ¬¾å½¢å¼' }) return } if (!form.value.receiptPaymentDate) { showNotify({ type: 'warning', message: 'è¯·éæ©æ¥æ¬¾æ¥æ' }) return } loading.value = true receiptPaymentSaveOrUpdate(form.value) .then(() => { showToast('æäº¤æå') // éç¥ä¸ä¸é¡µå·æ°æ°æ® const pages = getCurrentPages(); const prevPage = pages[pages.length - 2]; if (prevPage && prevPage.$vm && prevPage.$vm.getList) { prevPage.$vm.getList(); } uni.removeStorageSync('invoiceLedgerEditRow'); uni.navigateBack() }) .catch((error) => { loading.value = false }) } // åå§åæ°æ® const initData = () => { const rowStr = uni.getStorageSync('invoiceLedgerEditRow') const row = JSON.parse(rowStr) invoiceInfo({ id: row.id }).then((res) => { const data = res.data form.value = { ...data} form.value.invoiceLedgerId = form.value.id; form.value.id = ""; currentNoReceiptAmount.value = row.noReceiptAmount form.value.registrant = userStore.nickName }).catch(() => { showToast('è·åæ°æ®å¤±è´¥') }) } onMounted(() => { initData() }) </script> <style scoped lang="scss"> .account-detail { min-height: 100vh; background: #f8f9fa; padding-bottom: 5rem; } .footer-btns { position: fixed; left: 0; right: 0; bottom: 0; background: #fff; display: flex; justify-content: space-around; align-items: center; padding: 0.75rem 0; box-shadow: 0 -0.125rem 0.5rem rgba(0,0,0,0.05); z-index: 1000; } .cancel-btn { font-weight: 400; font-size: 1rem; color: #FFFFFF; width: 6.375rem; background: #C7C9CC; box-shadow: 0 0.25rem 0.625rem 0 rgba(3,88,185,0.2); border-radius: 2.5rem 2.5rem 2.5rem 2.5rem; } .save-btn { font-weight: 400; font-size: 1rem; color: #FFFFFF; width: 14rem; background: linear-gradient( 140deg, #00BAFF 0%, #006CFB 100%); box-shadow: 0 0.25rem 0.625rem 0 rgba(3,88,185,0.2); border-radius: 2.5rem 2.5rem 2.5rem 2.5rem; } // ååºå¼è°æ´ @media (max-width: 768px) { .submit-section { padding: 12px; } } .tip-text { padding: 4px 16px 0 16px; font-size: 12px; color: #888; } </style> src/pages/sales/receiptPayment/index.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,405 @@ <template> <view class="receipt-payment"> <!-- 页é¢å¤´é¨ --> <van-nav-bar title="忬¾ç»è®°" left-text="è¿å" left-arrow @click-left="goBack" fixed placeholder /> <!-- æç´¢åçéåºå --> <view class="search-filter-section"> <view class="search-bar"> <view class="search-input"> <input class="search-text" placeholder="客æ·åç§°/ååå·/项ç®åç§°" v-model="searchForm.searchText" confirm-type="search" /> </view> <view class="filter-button" @click="getList"> <up-icon name="search" size="24" color="#999"></up-icon> </view> </view> <!-- çéå¼å ³ --> <view class="switch-row"> <text class="switch-label">䏿¾ç¤ºå¾ 忬¾ä¸º0</text> <van-switch v-model="searchForm.status" @change="getList" size="18"/> </view> </view> <!-- å表åºå --> <view class="ledger-list" v-if="tableData.length > 0"> <view v-for="(item, index) in tableData" :key="index"> <view class="ledger-item"> <view class="item-header"> <view class="item-left"> <view class="document-icon"> <up-icon name="file-text" size="16" color="#ffffff"></up-icon> </view> <text class="item-id">{{ item.salesContractNo }}</text> </view> </view> <up-divider></up-divider> <view class="item-details"> <view class="detail-row"> <text class="detail-label">客æ·åç§°</text> <text class="detail-value">{{ item.customerName }}</text> </view> <view class="detail-row"> <text class="detail-label">客æ·ååå·</text> <text class="detail-value">{{ item.customerContractNo }}</text> </view> <view class="detail-row"> <text class="detail-label">项ç®åç§°</text> <text class="detail-value">{{ item.projectName }}</text> </view> <view class="detail-row"> <text class="detail-label">产å大类</text> <text class="detail-value">{{ item.productCategory }}</text> </view> <view class="detail-row"> <text class="detail-label">å票å·</text> <text class="detail-value">{{ item.invoiceNo || '-' }}</text> </view> <view class="detail-row"> <text class="detail-label">å票éé¢(å )</text> <text class="detail-value highlight">{{ formatNumber(item.invoiceTotal) }}</text> </view> <view class="detail-row"> <text class="detail-label">ç¨ç(%)</text> <text class="detail-value">{{ item.taxRate }}</text> </view> <view class="detail-row"> <text class="detail-label">忬¾éé¢(å )</text> <text class="detail-value highlight">{{ formatNumber(item.receiptPaymentAmountTotal) }}</text> </view> <view class="detail-row"> <text class="detail-label">å¾ åæ¬¾éé¢(å )</text> <text class="detail-value danger">{{ formatNumber(item.noReceiptAmount) }}</text> </view> </view> <!-- æä½æé® --> <view class="action-buttons"> <van-button type="primary" size="small" class="action-btn" :disabled="item.noReceiptAmount == 0" @click="openForm(item)" > æ°å¢å款 </van-button> </view> </view> </view> </view> <!-- æ æ°æ®æç¤º --> <view class="no-data" v-else> <text>ææ åæ¬¾æ°æ®</text> </view> <!-- 忬¾æ¹å¼éæ©å¨ --> <van-popup v-model:show="showPaymentType" position="bottom"> <van-picker :columns="receipt_payment_type" @confirm="onPaymentTypeConfirm" @cancel="showPaymentType = false" /> </van-popup> </view> </template> <script setup> import { ref } from 'vue' import { bindInvoiceNoRegPage, } from '@/api/salesManagement/receiptPayment' import useUserStore from '@/store/modules/user' import { showToast } from 'vant' import {onShow} from "@dcloudio/uni-app"; const userStore = useUserStore() // ååºå¼æ°æ® const tableData = ref([]) const tableLoading = ref(false) const showPaymentType = ref(false) const currentEditRow = ref(null) // æ¥è¯¢åæ°è®¾ç½®ä¸º-1è·åå ¨é¨æ°æ® const page = ref({ current: -1, size: -1 }) // æç´¢è¡¨å const searchForm = ref({ searchText: '', status: true, customerName: '', customerContractNo: '', projectName: '' }) // 忬¾æ¹å¼é项 const receipt_payment_type = ref([ { text: 'ç°é', value: '1' }, { text: 'é¶è¡è½¬è´¦', value: '2' }, { text: 'æ¯ç¥¨', value: '3' }, { text: 'å ¶ä»', value: '4' } ]) // æ ¼å¼åæ°å const formatNumber = (value) => { return parseFloat(value || 0).toFixed(2) } // è¿åä¸ä¸é¡µ const goBack = () => { uni.navigateBack() } // è·ååè¡¨æ°æ® const getList = () => { tableLoading.value = true bindInvoiceNoRegPage({ ...searchForm.value, ...page.value }) .then((res) => { tableLoading.value = false tableData.value = res.data.records || [] }) .catch(() => { tableLoading.value = false }) } // æå¼æ°å¢è¡¨å const openForm = (item) => { if (item.noReceiptAmount == 0) { showToast('æ éå忬¾') return } uni.setStorageSync('invoiceLedgerEditRow', JSON.stringify(item)) uni.navigateTo({ url: '/pages/sales/receiptPayment/add' }) } // ç¡®è®¤åæ¬¾æ¹å¼éæ© const onPaymentTypeConfirm = (value) => { if (currentEditRow.value) { currentEditRow.value.receiptPaymentType = value.value } showPaymentType.value = false } onShow(() => { getList() }) </script> <style scoped lang="scss"> .u-divider { margin: 0 !important; } .receipt-payment { min-height: 100vh; background: #f8f9fa; position: relative; } .search-filter-section { padding: 10px 20px; background: #ffffff; } .search-bar { display: flex; align-items: center; gap: 12px; } .search-input { flex: 1; background: #f5f5f5; border-radius: 24px; padding: 10px 16px; display: flex; align-items: center; gap: 8px; } .search-text { flex: 1; font-size: 14px; color: #333; background: transparent; border: none; outline: none; } .search-text::placeholder { color: #999; } .filter-button { width: 40px; height: 40px; border-radius: 8px; display: flex; align-items: center; justify-content: center; } .switch-row { padding: 8px; display: flex; align-items: center; justify-content: space-between; margin-top: 8px; } .switch-label { font-size: 14px; color: #333; } .ledger-list { padding: 20px; } .ledger-item { background: #ffffff; border-radius: 12px; margin-bottom: 16px; overflow: hidden; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); padding: 0 16px; } .item-header { padding: 16px 0; display: flex; align-items: center; justify-content: space-between; } .item-left { display: flex; align-items: center; gap: 8px; } .document-icon { width: 24px; height: 24px; background: #2979ff; border-radius: 4px; display: flex; align-items: center; justify-content: center; } .item-id { font-size: 14px; color: #333; font-weight: 500; } .item-details { padding: 16px 0; } .detail-row { display: flex; align-items: flex-end; justify-content: space-between; margin-bottom: 8px; &:last-child { margin-bottom: 0; } } .detail-label { font-size: 12px; color: #777777; min-width: 60px; } .detail-value { font-size: 12px; color: #000000; text-align: right; flex: 1; margin-left: 16px; display: flex; align-items: center; justify-content: flex-end; } .detail-value.highlight { color: #2979ff; font-weight: 500; } .detail-value.danger { color: #ee0a24; font-weight: 500; } .children-list { .children-title { font-size: 14px; font-weight: 500; color: #333; padding: 12px 0 8px 0; border-top: 1px solid #f0f0f0; } } .child-item { .child-details { padding: 12px 0; } .child-actions { display: flex; gap: 8px; padding: 8px 0 16px 0; justify-content: flex-end; } } .action-buttons { display: flex; gap: 12px; padding: 0 0 16px 0; justify-content: space-between; } .action-btn { flex: 1; display: flex; align-items: center; justify-content: center; gap: 8px; } .no-data { padding: 40px 0; text-align: center; color: #999; } </style> src/pages/sales/salesAccount/detail.vue
@@ -616,7 +616,7 @@ }; const setUserInfo = () => { form.value.entryPerson = userStore.id; form.value.entryPersonName = userStore.name; form.value.entryPersonName = userStore.nickName; // 设置å½å¤©æ¥æ const today = new Date() const year = today.getFullYear() src/store/modules/user.ts
@@ -2,6 +2,7 @@ import { getToken, setToken, removeToken } from "@/utils/auth"; import defAva from "@/static/images/profile.jpg"; import { defineStore } from "pinia"; import config from '@/config.js' export interface LoginForm { username: string; @@ -46,7 +47,7 @@ .then((res: any) => { const user = res.user let avatar = user.avatar || "" avatar = import.meta.env.VITE_APP_BASE_API + '/profile/' + avatar avatar = config.baseUrl + '/profile/' + avatar if (res.roles && res.roles.length > 0) { // éªè¯è¿åçrolesæ¯å¦æ¯ä¸ä¸ªé空æ°ç» this.roles = res.roles this.permissions = res.permissions