feat(财务与售后): 新增财务管理与售后管理模块
- 新增财务管理模块:收入管理、支出管理、借款管理的API接口和页面
- 新增售后管理模块:售后登记、售后处理、附件管理的完整功能
- 更新工作台菜单配置,添加财务和售后相关菜单项
- 修复生产调度表单中用户store导入路径问题
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | // åé¦ç»è®°-å页æ¥è¯¢ |
| | | export function afterSalesServiceListPage(query) { |
| | | return request({ |
| | | url: '/afterSalesService/listPage', |
| | | method: 'get', |
| | | params: query, |
| | | }) |
| | | } |
| | | // åé¦ç»è®°-å é¤ |
| | | export function afterSalesServiceDelete(query) { |
| | | return request({ |
| | | url: '/afterSalesService/delete', |
| | | method: 'delete', |
| | | data: query, |
| | | }) |
| | | } |
| | | // åé¦ç»è®°-æ°å¢ |
| | | export function afterSalesServiceAdd(query) { |
| | | return request({ |
| | | url: '/afterSalesService/add', |
| | | method: 'post', |
| | | data: query, |
| | | }) |
| | | } |
| | | // åé¦ç»è®°-æ´æ° |
| | | export function afterSalesServiceUpdate(query) { |
| | | return request({ |
| | | url: '/afterSalesService/update', |
| | | method: 'post', |
| | | data: query, |
| | | }) |
| | | } |
| | | // å®åå¤ç-æäº¤å¤ç |
| | | export function afterSalesServiceDispose(query) { |
| | | return request({ |
| | | url: '/afterSalesService/dispose', |
| | | method: 'post', |
| | | data: query, |
| | | }) |
| | | } |
| | | |
| | | // å®åå¤ç-éä»¶å表 |
| | | export function afterSalesServiceFileListPage(query) { |
| | | return request({ |
| | | url: '/afterSalesService/file/listPage', |
| | | method: 'get', |
| | | params: query, |
| | | }) |
| | | } |
| | | // å®åå¤ç-éä»¶æ°å¢ |
| | | export function afterSalesServiceFileAdd(data) { |
| | | return request({ |
| | | url: '/afterSalesService/file/add', |
| | | method: 'post', |
| | | data, |
| | | }) |
| | | } |
| | | // å®åå¤ç-éä»¶å é¤ |
| | | export function afterSalesServiceFileDel(id) { |
| | | return request({ |
| | | url: `/afterSalesService/file/del/${id}`, |
| | | method: 'delete', |
| | | }) |
| | | } |
| | | |
| | | // æ¥è¯¢ææå®¢æ·ä¿¡æ¯ |
| | | export function getAllCustomerList(query) { |
| | | return request({ |
| | | url: '/basic/customer/list', |
| | | method: 'get', |
| | | params: query, |
| | | }) |
| | | } |
| | | |
| | | // æ ¹æ®å®¢æ·æ¥è¯¢éå®è®¢åå· |
| | | export function getSalesLedger(query) { |
| | | return request({ |
| | | url: '/afterSalesService/listSalesLedger', |
| | | method: 'get', |
| | | params: query, |
| | | }) |
| | | } |
| | | |
| | | // è·åç»è®¡æ°æ® |
| | | export function getSalesLedgerDetail(query) { |
| | | return request({ |
| | | url: '/afterSalesService/count', |
| | | method: 'get', |
| | | params: query, |
| | | }) |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | export const listPage = (params) => { |
| | | return request({ |
| | | url: "/account/accountExpense/listPage", |
| | | method: "get", |
| | | params, |
| | | }); |
| | | }; |
| | | |
| | | export function add(data) { |
| | | return request({ |
| | | url: "/account/accountExpense/add", |
| | | method: "post", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | export function update(data) { |
| | | return request({ |
| | | url: "/account/accountExpense/update", |
| | | method: "post", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | export const delAccountExpense = (data) => { |
| | | return request({ |
| | | url: "account/accountExpense/del", |
| | | method: "delete", |
| | | data, |
| | | }); |
| | | }; |
| | | |
| | | export const getAccountExpense = (id) => { |
| | | return request({ |
| | | url: `/account/accountExpense/${id}`, |
| | | method: "get", |
| | | }); |
| | | }; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | export const listPage = (params) => { |
| | | return request({ |
| | | url: "/borrowInfo/listPage", |
| | | method: "get", |
| | | params, |
| | | }); |
| | | }; |
| | | |
| | | export function add(data) { |
| | | return request({ |
| | | url: "/borrowInfo/add", |
| | | method: "post", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | export function update(data) { |
| | | return request({ |
| | | url: "/borrowInfo/update", |
| | | method: "post", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | export const delAccountLoan = (data) => { |
| | | return request({ |
| | | url: "/borrowInfo/delete", |
| | | method: "delete", |
| | | data, |
| | | }); |
| | | }; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | export const listPage = (params) => { |
| | | return request({ |
| | | url: "/account/accountIncome/listPage", |
| | | method: "get", |
| | | params, |
| | | }); |
| | | }; |
| | | |
| | | export function add(data) { |
| | | return request({ |
| | | url: "/account/accountIncome/add", |
| | | method: "post", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | export function update(data) { |
| | | return request({ |
| | | url: "/account/accountIncome/update", |
| | | method: "post", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | export const delAccountIncome = (data) => { |
| | | return request({ |
| | | url: "account/accountIncome/del", |
| | | method: "delete", |
| | | data, |
| | | }); |
| | | }; |
| | | |
| | | export const getAccountIncome = (id) => { |
| | | return request({ |
| | | url: `/account/accountIncome/${id}`, |
| | | method: "get", |
| | | }); |
| | | }; |
| | |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/financialManagement/revenueManagement/index", |
| | | "style": { |
| | | "navigationBarTitleText": "æ¶å
¥ç®¡ç", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/financialManagement/revenueManagement/edit", |
| | | "style": { |
| | | "navigationBarTitleText": "æ¶å
¥ç¼è¾", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/financialManagement/expenseManagement/index", |
| | | "style": { |
| | | "navigationBarTitleText": "æ¯åºç®¡ç", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/financialManagement/expenseManagement/edit", |
| | | "style": { |
| | | "navigationBarTitleText": "æ¯åºç¼è¾", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/financialManagement/loanManagement/index", |
| | | "style": { |
| | | "navigationBarTitleText": "忬¾ç®¡ç", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/financialManagement/loanManagement/edit", |
| | | "style": { |
| | | "navigationBarTitleText": "忬¾ç¼è¾", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/index", |
| | | "style": { |
| | | "navigationBarTitleText": "é¦é¡µ", |
| | |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/customerService/feedbackRegistration/index", |
| | | "style": { |
| | | "navigationBarTitleText": "å®åç»è®°", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/customerService/feedbackRegistration/edit", |
| | | "style": { |
| | | "navigationBarTitleText": "å®åå详æ
", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/customerService/afterSalesHandling/index", |
| | | "style": { |
| | | "navigationBarTitleText": "å®åå¤ç", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/customerService/afterSalesHandling/handle", |
| | | "style": { |
| | | "navigationBarTitleText": "å®åå¤ç详æ
", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/customerService/afterSalesHandling/fileList", |
| | | "style": { |
| | | "navigationBarTitleText": "å®åéä»¶", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/qualityManagement/finalInspection/add", |
| | | "style": { |
| | | "navigationBarTitleText": "åºåæ£éªæ·»å ", |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="file-list-page"> |
| | | <PageHeader title="å®åéä»¶" @back="goBack" /> |
| | | |
| | | <view class="file-list-container"> |
| | | <view v-if="fileList.length > 0" class="file-list"> |
| | | <view v-for="(file, index) in fileList" :key="file.id || index" class="file-item"> |
| | | <view class="file-info"> |
| | | <text class="file-name">{{ file.name }}</text> |
| | | </view> |
| | | <view class="file-actions"> |
| | | <u-button size="small" type="info" plain @click="downloadFile(file)">ä¸è½½å¹¶é¢è§</u-button> |
| | | <u-button size="small" type="error" plain @click="confirmDelete(file)">å é¤</u-button> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <view v-else class="empty-state"> |
| | | <up-icon name="document" size="64" color="#c0c4cc" /> |
| | | <text class="empty-text">ææ éä»¶</text> |
| | | </view> |
| | | </view> |
| | | |
| | | <view class="upload-button" @click="chooseFile"> |
| | | <up-icon name="plus" size="24" color="#ffffff" /> |
| | | <text class="upload-text">ä¸ä¼ éä»¶</text> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, onMounted } from "vue"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import config from "@/config"; |
| | | import { getToken } from "@/utils/auth"; |
| | | import { afterSalesServiceFileListPage, afterSalesServiceFileDel } from "@/api/customerService/index"; |
| | | |
| | | const fileList = ref([]); |
| | | const afterSalesServiceId = ref(""); |
| | | |
| | | const goBack = () => { |
| | | uni.navigateBack(); |
| | | }; |
| | | |
| | | const showToast = (message) => { |
| | | uni.showToast({ |
| | | title: message, |
| | | icon: "none", |
| | | }); |
| | | }; |
| | | |
| | | const normalizeFileRow = (row) => { |
| | | return { |
| | | id: row?.id, |
| | | name: row?.name || row?.fileName || "-", |
| | | url: row?.url || row?.fileUrl || "", |
| | | }; |
| | | }; |
| | | |
| | | const getFileList = () => { |
| | | if (!afterSalesServiceId.value) { |
| | | fileList.value = []; |
| | | return; |
| | | } |
| | | uni.showLoading({ title: "å è½½ä¸...", mask: true }); |
| | | afterSalesServiceFileListPage({ |
| | | afterSalesServiceId: afterSalesServiceId.value, |
| | | current: 1, |
| | | size: 100, |
| | | }) |
| | | .then((res) => { |
| | | const records = res?.data?.records ?? res?.records ?? []; |
| | | const list = Array.isArray(records) ? records : []; |
| | | fileList.value = list.map(normalizeFileRow); |
| | | }) |
| | | .catch(() => { |
| | | showToast("è·åéä»¶å表失败"); |
| | | fileList.value = []; |
| | | }) |
| | | .finally(() => { |
| | | uni.hideLoading(); |
| | | }); |
| | | }; |
| | | |
| | | const chooseFile = () => { |
| | | if (!afterSalesServiceId.value) { |
| | | showToast("缺å°å®ååID"); |
| | | return; |
| | | } |
| | | uni.chooseImage({ |
| | | count: 9, |
| | | sizeType: ["original", "compressed"], |
| | | sourceType: ["album", "camera"], |
| | | success: (res) => { |
| | | uploadFiles(res.tempFiles || []); |
| | | }, |
| | | fail: () => { |
| | | showToast("éæ©æä»¶å¤±è´¥"); |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | const uploadFiles = (tempFiles) => { |
| | | if (!Array.isArray(tempFiles) || tempFiles.length === 0) return; |
| | | tempFiles.forEach((tempFile) => { |
| | | uni.showLoading({ title: "ä¸ä¼ ä¸...", mask: true }); |
| | | uni.uploadFile({ |
| | | url: config.baseUrl + "/afterSalesService/file/upload", |
| | | filePath: tempFile.path, |
| | | name: "file", |
| | | formData: { |
| | | id: String(afterSalesServiceId.value), |
| | | }, |
| | | header: { |
| | | Authorization: "Bearer " + getToken(), |
| | | }, |
| | | success: (uploadRes) => { |
| | | uni.hideLoading(); |
| | | try { |
| | | const data = JSON.parse(uploadRes.data || "{}"); |
| | | if (data.code === 200 || data.code === undefined) { |
| | | showToast("ä¸ä¼ æå"); |
| | | getFileList(); |
| | | return; |
| | | } |
| | | showToast(data.msg || "ä¸ä¼ 失败"); |
| | | } catch (e) { |
| | | showToast("ä¸ä¼ 失败"); |
| | | } |
| | | }, |
| | | fail: () => { |
| | | uni.hideLoading(); |
| | | showToast("ä¸ä¼ 失败"); |
| | | }, |
| | | }); |
| | | }); |
| | | }; |
| | | |
| | | const downloadFile = (file) => { |
| | | if (!file?.url) { |
| | | showToast("æä»¶å°å为空"); |
| | | return; |
| | | } |
| | | const url = |
| | | config.baseUrl + |
| | | "/common/download?fileName=" + |
| | | encodeURIComponent(file.url) + |
| | | "&delete=true"; |
| | | |
| | | uni |
| | | .downloadFile({ |
| | | url, |
| | | responseType: "blob", |
| | | header: { Authorization: "Bearer " + getToken() }, |
| | | }) |
| | | .then((res) => { |
| | | const osType = uni.getStorageSync("deviceInfo")?.osName; |
| | | const filePath = res.tempFilePath; |
| | | if (osType === "ios") { |
| | | uni.openDocument({ |
| | | filePath, |
| | | showMenu: true, |
| | | }); |
| | | } else { |
| | | uni.saveFile({ |
| | | tempFilePath: filePath, |
| | | success: (fileRes) => { |
| | | setTimeout(() => { |
| | | uni.openDocument({ |
| | | filePath: fileRes.savedFilePath, |
| | | }); |
| | | }, 300); |
| | | }, |
| | | fail: () => { |
| | | showToast("ä¿å失败"); |
| | | }, |
| | | }); |
| | | } |
| | | }) |
| | | .catch(() => { |
| | | showToast("ä¸è½½å¤±è´¥"); |
| | | }); |
| | | }; |
| | | |
| | | const confirmDelete = (file) => { |
| | | uni.showModal({ |
| | | title: "å é¤ç¡®è®¤", |
| | | content: `ç¡®å®è¦å é¤éä»¶ \"${file.name}\" åï¼`, |
| | | success: (res) => { |
| | | if (res.confirm) { |
| | | deleteFile(file); |
| | | } |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | const deleteFile = (file) => { |
| | | if (!file?.id) return; |
| | | uni.showLoading({ title: "å é¤ä¸...", mask: true }); |
| | | afterSalesServiceFileDel(file.id) |
| | | .then((res) => { |
| | | if (res?.code === 200 || res?.code === undefined) { |
| | | showToast("å 餿å"); |
| | | getFileList(); |
| | | return; |
| | | } |
| | | showToast(res?.msg || "å é¤å¤±è´¥"); |
| | | }) |
| | | .catch(() => { |
| | | showToast("å é¤å¤±è´¥"); |
| | | }) |
| | | .finally(() => { |
| | | uni.hideLoading(); |
| | | }); |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | afterSalesServiceId.value = String(uni.getStorageSync("afterSalesServiceFileId") || ""); |
| | | getFileList(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/styles/sales-common.scss"; |
| | | |
| | | .file-list-page { |
| | | min-height: 100vh; |
| | | background: #f8f9fa; |
| | | padding-bottom: 100rpx; |
| | | } |
| | | |
| | | .file-list-container { |
| | | padding: 20rpx; |
| | | } |
| | | |
| | | .file-list { |
| | | background: #ffffff; |
| | | border-radius: 8rpx; |
| | | overflow: hidden; |
| | | box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05); |
| | | } |
| | | |
| | | .file-item { |
| | | display: flex; |
| | | align-items: center; |
| | | padding: 20rpx; |
| | | border-bottom: 1rpx solid #f0f0f0; |
| | | } |
| | | |
| | | .file-info { |
| | | flex: 1; |
| | | margin-right: 20rpx; |
| | | } |
| | | |
| | | .file-name { |
| | | font-size: 28rpx; |
| | | color: #333; |
| | | font-weight: 500; |
| | | display: block; |
| | | } |
| | | |
| | | .file-actions { |
| | | display: flex; |
| | | gap: 16rpx; |
| | | } |
| | | |
| | | .empty-state { |
| | | padding: 80rpx 0; |
| | | text-align: center; |
| | | } |
| | | |
| | | .empty-text { |
| | | display: block; |
| | | margin-top: 20rpx; |
| | | color: #999; |
| | | font-size: 28rpx; |
| | | } |
| | | |
| | | .upload-button { |
| | | position: fixed; |
| | | bottom: calc(30rpx + env(safe-area-inset-bottom)); |
| | | right: 30rpx; |
| | | height: 88rpx; |
| | | padding: 0 28rpx; |
| | | background: #2979ff; |
| | | border-radius: 44rpx; |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 14rpx; |
| | | box-shadow: 0 4rpx 16rpx rgba(41, 121, 255, 0.3); |
| | | z-index: 1000; |
| | | } |
| | | |
| | | .upload-text { |
| | | color: #ffffff; |
| | | font-size: 28rpx; |
| | | font-weight: 500; |
| | | } |
| | | </style> |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="after-sales-handle"> |
| | | <PageHeader :title="operationType === 'approve' ? 'å®åå¤ç' : 'å®å详æ
'" @back="goBack" /> |
| | | |
| | | <view class="form-container"> |
| | | <up-form ref="formRef" :model="form" :rules="rules" label-width="100"> |
| | | <up-form-item label="å馿¥æ" prop="feedbackDate"> |
| | | <up-input v-model="form.feedbackDate" disabled /> |
| | | </up-form-item> |
| | | |
| | | <up-form-item label="ç»è®°äºº" prop="checkNickName"> |
| | | <up-input v-model="form.checkNickName" disabled /> |
| | | </up-form-item> |
| | | |
| | | <up-form-item label="客æ·åç§°" prop="customerName"> |
| | | <up-input v-model="form.customerName" disabled /> |
| | | </up-form-item> |
| | | |
| | | <up-form-item label="é®é¢æè¿°" prop="proDesc" required> |
| | | <up-textarea v-model="form.proDesc" :disabled="operationType === 'view'" placeholder="请è¾å
¥é®é¢æè¿°" count autoHeight></up-textarea> |
| | | </up-form-item> |
| | | |
| | | <up-form-item label="å¤çç»æ" prop="disRes" required> |
| | | <up-textarea v-model="form.disRes" :disabled="operationType === 'view'" placeholder="请è¾å
¥å¤çç»æ" count autoHeight></up-textarea> |
| | | </up-form-item> |
| | | |
| | | <up-form-item label="å¤ç人" prop="disposeNickName" v-if="form.disposeNickName || operationType === 'approve'"> |
| | | <up-input v-model="form.disposeNickName" disabled /> |
| | | </up-form-item> |
| | | |
| | | <up-form-item label="å¤çæ¥æ" prop="disDate" v-if="form.disDate || operationType === 'approve'"> |
| | | <up-input v-model="form.disDate" disabled /> |
| | | </up-form-item> |
| | | </up-form> |
| | | </view> |
| | | |
| | | <FooterButtons :show="operationType === 'approve'" cancelText="åæ¶" confirmText="æäº¤" @cancel="goBack" @confirm="submitForm" /> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, onMounted } from 'vue'; |
| | | import { afterSalesServiceDispose } from '@/api/customerService/index'; |
| | | import PageHeader from '@/components/PageHeader.vue'; |
| | | import FooterButtons from '@/components/FooterButtons.vue'; |
| | | import useUserStore from '@/store/modules/user'; |
| | | |
| | | const userStore = useUserStore(); |
| | | const operationType = ref('view'); |
| | | const formRef = ref(null); |
| | | const form = reactive({ |
| | | id: null, |
| | | feedbackDate: '', |
| | | checkUserId: null, |
| | | checkNickName: '', |
| | | customerName: '', |
| | | proDesc: '', |
| | | disRes: '', |
| | | disposeUserId: null, |
| | | disposeNickName: '', |
| | | disDate: '', |
| | | }); |
| | | |
| | | const rules = { |
| | | proDesc: [{ required: true, message: '请è¾å
¥é®é¢æè¿°', trigger: 'blur' }], |
| | | disRes: [{ required: true, message: '请è¾å
¥å¤çç»æ', trigger: 'blur' }], |
| | | }; |
| | | |
| | | const goBack = () => { |
| | | uni.navigateBack(); |
| | | }; |
| | | |
| | | const submitForm = () => { |
| | | formRef.value.validate().then(valid => { |
| | | if (valid) { |
| | | afterSalesServiceDispose(form).then(() => { |
| | | uni.showToast({ title: 'å¤çæå', icon: 'success' }); |
| | | setTimeout(() => goBack(), 1500); |
| | | }); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | operationType.value = uni.getStorageSync('afterSalesHandleType') || 'view'; |
| | | const dataStr = uni.getStorageSync('afterSalesHandleData'); |
| | | |
| | | if (dataStr) { |
| | | const data = JSON.parse(dataStr); |
| | | Object.assign(form, data); |
| | | |
| | | // Normalize field names if they differ between API and web view |
| | | if (!form.proDesc) form.proDesc = data.disRes; // Fallback to disRes if proDesc is empty |
| | | |
| | | if (operationType.value === 'approve') { |
| | | if (!form.disposeUserId) { |
| | | form.disposeUserId = userStore.id; |
| | | form.disposeNickName = userStore.nickName; |
| | | } |
| | | if (!form.disDate) { |
| | | const now = new Date(); |
| | | form.disDate = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`; |
| | | } |
| | | } |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .after-sales-handle { |
| | | min-height: 100vh; |
| | | background: #f8f9fa; |
| | | padding-bottom: 100px; |
| | | } |
| | | |
| | | .form-container { |
| | | background: #ffffff; |
| | | padding: 10px 20px; |
| | | margin-top: 10px; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="after-sales-handling"> |
| | | <PageHeader title="å®åå¤ç" @back="goBack" /> |
| | | |
| | | <!-- æç´¢åçéåºå --> |
| | | <view class="search-section"> |
| | | <view class="search-bar"> |
| | | <view class="search-input"> |
| | | <up-input class="search-text" placeholder="请è¾å
¥å·¥åç¼å·æç´¢" v-model="searchForm.afterSalesServiceNo" @change="handleQuery" clearable /> |
| | | </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="tableData.length > 0"> |
| | | <view v-for="(item, index) in tableData" :key="index" class="ledger-item" @click="openHandleForm('view', 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.afterSalesServiceNo }}</text> |
| | | </view> |
| | | <view class="item-tag"> |
| | | <up-tag :text="getStatusLabel(item.status)" :type="getStatusType(item.status)" size="mini"></up-tag> |
| | | </view> |
| | | </view> |
| | | <up-divider></up-divider> |
| | | <view class="item-details"> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">éå®åå·</text> |
| | | <text class="detail-value">{{ item.salesContractNo }}</text> |
| | | </view> |
| | | <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.feedbackDate }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">å®åç±»å</text> |
| | | <text class="detail-value">{{ getDictLabel(post_sale_waiting_list, item.serviceType) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">ç´§æ¥ç¨åº¦</text> |
| | | <text class="detail-value">{{ getDictLabel(degree_of_urgency, item.urgency) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">é®é¢æè¿°</text> |
| | | <text class="detail-value">{{ item.proDesc || item.disRes }}</text> |
| | | </view> |
| | | <view class="detail-row" v-if="item.status === 2"> |
| | | <text class="detail-label">å¤çç»æ</text> |
| | | <text class="detail-value highlight">{{ item.disRes }}</text> |
| | | </view> |
| | | </view> |
| | | <up-divider></up-divider> |
| | | <view class="detail-buttons"> |
| | | <up-button size="small" type="primary" v-if="item.status === 1" @click.stop="openHandleForm('approve', item)">å¤ç</up-button> |
| | | <up-button size="small" type="primary" plain @click.stop="openHandleForm('view', item)">æ¥ç</up-button> |
| | | <up-button size="small" type="info" plain @click.stop="openFiles(item)">éä»¶</up-button> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view v-else class="no-data"> |
| | | <text>ææ å®åå¤çæ°æ®</text> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, computed } from 'vue'; |
| | | import { onShow } from '@dcloudio/uni-app'; |
| | | import { afterSalesServiceListPage } from '@/api/customerService/index'; |
| | | import PageHeader from '@/components/PageHeader.vue'; |
| | | import { useDict } from '@/utils/dict'; |
| | | |
| | | const { post_sale_waiting_list, degree_of_urgency } = useDict('post_sale_waiting_list', 'degree_of_urgency'); |
| | | |
| | | const searchForm = reactive({ |
| | | afterSalesServiceNo: '', |
| | | status: '', |
| | | }); |
| | | |
| | | const tableData = ref([]); |
| | | const loading = ref(false); |
| | | const page = reactive({ |
| | | current: 1, |
| | | size: 20, |
| | | total: 0, |
| | | }); |
| | | |
| | | const goBack = () => { |
| | | uni.navigateBack(); |
| | | }; |
| | | |
| | | const handleQuery = () => { |
| | | page.current = 1; |
| | | getList(); |
| | | }; |
| | | |
| | | const getList = () => { |
| | | loading.value = true; |
| | | uni.showLoading({ title: 'å è½½ä¸...' }); |
| | | |
| | | afterSalesServiceListPage({ ...searchForm, ...page }) |
| | | .then((res) => { |
| | | const records = res?.data?.records ?? res?.records ?? []; |
| | | const total = res?.data?.total ?? res?.total ?? 0; |
| | | tableData.value = Array.isArray(records) ? records : []; |
| | | page.total = Number.isFinite(Number(total)) ? Number(total) : 0; |
| | | }) |
| | | .finally(() => { |
| | | loading.value = false; |
| | | uni.hideLoading(); |
| | | }); |
| | | }; |
| | | |
| | | const getStatusLabel = (status) => { |
| | | if (status === 1) return 'å¾
å¤ç'; |
| | | if (status === 2) return 'å·²å¤ç'; |
| | | return 'æªç¥'; |
| | | }; |
| | | |
| | | const getStatusType = (status) => { |
| | | if (status === 1) return 'error'; |
| | | if (status === 2) return 'success'; |
| | | return 'info'; |
| | | }; |
| | | |
| | | const getDictLabel = (dict, value) => { |
| | | if (!dict || !dict.value) return value; |
| | | const item = dict.value.find(i => i.value == value); |
| | | return item ? item.label : value; |
| | | }; |
| | | |
| | | const openHandleForm = (type, row) => { |
| | | uni.setStorageSync('afterSalesHandleType', type); |
| | | uni.setStorageSync('afterSalesHandleData', JSON.stringify(row)); |
| | | uni.navigateTo({ |
| | | url: '/pages/customerService/afterSalesHandling/handle' |
| | | }); |
| | | }; |
| | | |
| | | const openFiles = (row) => { |
| | | uni.setStorageSync('afterSalesFileData', JSON.stringify(row)); |
| | | uni.setStorageSync('afterSalesServiceFileId', row?.id); |
| | | uni.navigateTo({ |
| | | url: '/pages/customerService/afterSalesHandling/fileList' |
| | | }); |
| | | }; |
| | | |
| | | onShow(() => { |
| | | getList(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/styles/sales-common.scss"; |
| | | |
| | | .after-sales-handling { |
| | | min-height: 100vh; |
| | | background: #f8f9fa; |
| | | } |
| | | |
| | | .detail-buttons { |
| | | display: flex; |
| | | gap: 10px; |
| | | justify-content: flex-end; |
| | | padding: 12px 0; |
| | | } |
| | | |
| | | .ledger-item { |
| | | padding: 0 16px; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="after-sales-edit"> |
| | | <PageHeader :title="pageTitle" @back="goBack" /> |
| | | |
| | | <view class="form-container"> |
| | | <up-form ref="formRef" :model="form" :rules="isReadonly ? {} : rules" label-width="100"> |
| | | <up-form-item label="客æ·åç§°" prop="customerName" required @click="openCustomerPicker"> |
| | | <up-input v-model="form.customerName" readonly placeholder="è¯·éæ©å®¢æ·" /> |
| | | <template #right> |
| | | <up-icon v-if="!isReadonly" name="arrow-right" @click="openCustomerPicker"></up-icon> |
| | | </template> |
| | | </up-form-item> |
| | | |
| | | <up-form-item label="å®åç±»å" prop="serviceType" required @click="openServiceTypePicker"> |
| | | <up-input v-model="serviceTypeLabel" readonly placeholder="è¯·éæ©å®åç±»å" /> |
| | | <template #right> |
| | | <up-icon v-if="!isReadonly" name="arrow-right" @click="openServiceTypePicker"></up-icon> |
| | | </template> |
| | | </up-form-item> |
| | | |
| | | <up-form-item label="å
³èéå®å" prop="salesContractNo" required @click="openSalesOrderPicker"> |
| | | <up-input v-model="form.salesContractNo" readonly placeholder="è¯·éæ©éå®åå·" /> |
| | | <template #right> |
| | | <up-icon v-if="!isReadonly" name="arrow-right" @click="openSalesOrderPicker"></up-icon> |
| | | </template> |
| | | </up-form-item> |
| | | |
| | | <up-form-item label="ç´§æ¥ç¨åº¦" prop="urgency" required @click="openUrgencyPicker"> |
| | | <up-input v-model="urgencyLabel" readonly placeholder="è¯·éæ©ç´§æ¥ç¨åº¦" /> |
| | | <template #right> |
| | | <up-icon v-if="!isReadonly" name="arrow-right" @click="openUrgencyPicker"></up-icon> |
| | | </template> |
| | | </up-form-item> |
| | | |
| | | <up-form-item label="é®é¢æè¿°" prop="disRes"> |
| | | <up-textarea v-model="form.disRes" :disabled="isReadonly" placeholder="请è¾å
¥é®é¢æè¿°" count autoHeight></up-textarea> |
| | | </up-form-item> |
| | | </up-form> |
| | | |
| | | <!-- å
³è产ååºå --> |
| | | <view class="product-section"> |
| | | <view class="section-header"> |
| | | <up-button type="primary" size="small" @click="showProductSelect = true" v-if="!isReadonly && form.salesContractNo">éæ©äº§å</up-button> |
| | | </view> |
| | | |
| | | <view class="product-list" v-if="tableData.length > 0"> |
| | | <view v-for="(item, index) in tableData" :key="index" class="product-item"> |
| | | <view class="product-info"> |
| | | <view class="info-row"> |
| | | <text class="info-label">产ååç±»ï¼</text> |
| | | <text class="info-value">{{ item.productCategory }}</text> |
| | | </view> |
| | | <view class="info-row"> |
| | | <text class="info-label">è§æ ¼åå·ï¼</text> |
| | | <text class="info-value">{{ item.specificationModel }}</text> |
| | | </view> |
| | | <view class="info-row"> |
| | | <text class="info-label">åä½ï¼</text> |
| | | <text class="info-value">{{ item.unit }}</text> |
| | | </view> |
| | | <view class="info-row"> |
| | | <text class="info-label">æ°éï¼</text> |
| | | <text class="info-value">{{ item.quantity }}</text> |
| | | </view> |
| | | </view> |
| | | <view class="product-action" v-if="!isReadonly"> |
| | | <up-icon name="trash" color="#fa3534" size="20" @click="removeProduct(index)"></up-icon> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view v-else class="no-product"> |
| | | <text>ææ å
³è产å</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- åç§éæ©å¨ --> |
| | | <up-action-sheet :show="showCustomerPicker" :actions="customerActions" title="鿩客æ·" @select="onCustomerSelect" @close="showCustomerPicker = false" /> |
| | | <up-action-sheet :show="showServiceTypePicker" :actions="serviceTypeActions" title="éæ©å®åç±»å" @select="onServiceTypeSelect" @close="showServiceTypePicker = false" /> |
| | | <up-action-sheet :show="showSalesOrderPicker" :actions="salesOrderActions" title="éæ©å
³èéå®å" @select="onSalesOrderSelect" @close="showSalesOrderPicker = false" /> |
| | | <up-action-sheet :show="showUrgencyPicker" :actions="urgencyActions" title="éæ©ç´§æ¥ç¨åº¦" @select="onUrgencySelect" @close="showUrgencyPicker = false" /> |
| | | |
| | | <!-- 产åéæ©å¼¹çª --> |
| | | <up-popup :show="showProductSelect" mode="bottom" @close="showProductSelect = false" round="10"> |
| | | <view class="product-select-popup"> |
| | | <view class="popup-header"> |
| | | <text class="popup-title">éæ©äº§å</text> |
| | | <up-icon name="close" size="20" @click="showProductSelect = false"></up-icon> |
| | | </view> |
| | | <scroll-view scroll-y class="product-scroll"> |
| | | <view v-for="(item, index) in availableProducts" :key="index" class="selectable-product" @click="toggleProduct(item)"> |
| | | <view class="product-checkbox"> |
| | | <up-icon :name="isProductSelected(item) ? 'checkbox-mark' : 'minus-circle'" :color="isProductSelected(item) ? '#2979ff' : '#ccc'" size="20"></up-icon> |
| | | </view> |
| | | <view class="product-details"> |
| | | <text class="p-name">{{ item.productCategory }}</text> |
| | | <text class="p-model">{{ item.specificationModel }} | {{ item.unit }}</text> |
| | | </view> |
| | | </view> |
| | | </scroll-view> |
| | | <view class="popup-footer"> |
| | | <up-button type="primary" @click="showProductSelect = false">ç¡®å®</up-button> |
| | | </view> |
| | | </view> |
| | | </up-popup> |
| | | |
| | | <FooterButtons :show="!isReadonly" @cancel="goBack" @confirm="submitForm" /> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, computed, onMounted } from 'vue'; |
| | | import { getAllCustomerList, getSalesLedger, afterSalesServiceAdd, afterSalesServiceUpdate } from '@/api/customerService/index'; |
| | | import PageHeader from '@/components/PageHeader.vue'; |
| | | import FooterButtons from '@/components/FooterButtons.vue'; |
| | | import { useDict } from '@/utils/dict'; |
| | | import useUserStore from '@/store/modules/user'; |
| | | |
| | | const userStore = useUserStore(); |
| | | const { post_sale_waiting_list, degree_of_urgency } = useDict('post_sale_waiting_list', 'degree_of_urgency'); |
| | | |
| | | const operationType = ref('add'); |
| | | const isReadonly = computed(() => operationType.value === 'view'); |
| | | const pageTitle = computed(() => { |
| | | if (operationType.value === 'view') return 'å®åå详æ
'; |
| | | if (operationType.value === 'add') return 'æ°å¢å®åå'; |
| | | return 'ç¼è¾å®åå'; |
| | | }); |
| | | const formRef = ref(null); |
| | | const form = reactive({ |
| | | id: null, |
| | | customerName: '', |
| | | customerId: null, |
| | | serviceType: '', |
| | | salesContractNo: '', |
| | | salesLedgerId: null, |
| | | urgency: '', |
| | | disRes: '', |
| | | productModelIds: '', |
| | | checkUserId: null, |
| | | feedbackDate: '', |
| | | }); |
| | | |
| | | const rules = { |
| | | customerName: [{ required: true, message: 'è¯·éæ©å®¢æ·', trigger: 'change' }], |
| | | serviceType: [{ required: true, message: 'è¯·éæ©å®åç±»å', trigger: 'change' }], |
| | | salesContractNo: [{ required: true, message: 'è¯·éæ©å
³èéå®å', trigger: 'change' }], |
| | | urgency: [{ required: true, message: 'è¯·éæ©ç´§æ¥ç¨åº¦', trigger: 'change' }], |
| | | }; |
| | | |
| | | const tableData = ref([]); |
| | | const customerList = ref([]); |
| | | const salesOrderList = ref([]); |
| | | const availableProducts = ref([]); |
| | | |
| | | const showCustomerPicker = ref(false); |
| | | const showServiceTypePicker = ref(false); |
| | | const showSalesOrderPicker = ref(false); |
| | | const showUrgencyPicker = ref(false); |
| | | const showProductSelect = ref(false); |
| | | |
| | | const customerActions = computed(() => customerList.value.map(item => ({ name: item.customerName, value: item.id }))); |
| | | const serviceTypeActions = computed(() => (post_sale_waiting_list.value || []).map(item => ({ name: item.label, value: item.value }))); |
| | | const salesOrderActions = computed(() => salesOrderList.value.map(item => ({ name: item.salesContractNo, value: item.salesContractNo, id: item.id, products: item.productData }))); |
| | | const urgencyActions = computed(() => (degree_of_urgency.value || []).map(item => ({ name: item.label, value: item.value }))); |
| | | |
| | | const serviceTypeLabel = computed(() => { |
| | | const item = (post_sale_waiting_list.value || []).find(i => i.value == form.serviceType); |
| | | return item ? item.label : ''; |
| | | }); |
| | | |
| | | const urgencyLabel = computed(() => { |
| | | const item = (degree_of_urgency.value || []).find(i => i.value == form.urgency); |
| | | return item ? item.label : ''; |
| | | }); |
| | | |
| | | const goBack = () => { |
| | | uni.navigateBack(); |
| | | }; |
| | | |
| | | const openCustomerPicker = () => { |
| | | if (isReadonly.value) return; |
| | | showCustomerPicker.value = true; |
| | | }; |
| | | const openServiceTypePicker = () => { |
| | | if (isReadonly.value) return; |
| | | showServiceTypePicker.value = true; |
| | | }; |
| | | const openSalesOrderPicker = () => { |
| | | if (isReadonly.value) return; |
| | | if (!form.customerName) { |
| | | uni.showToast({ title: '请å
鿩客æ·', icon: 'none' }); |
| | | return; |
| | | } |
| | | showSalesOrderPicker.value = true; |
| | | }; |
| | | const openUrgencyPicker = () => { |
| | | if (isReadonly.value) return; |
| | | showUrgencyPicker.value = true; |
| | | }; |
| | | |
| | | const onCustomerSelect = (item) => { |
| | | form.customerName = item.name; |
| | | form.customerId = item.value; |
| | | form.salesContractNo = ''; |
| | | form.salesLedgerId = null; |
| | | tableData.value = []; |
| | | availableProducts.value = []; |
| | | fetchSalesOrders(item.name); |
| | | }; |
| | | |
| | | const onServiceTypeSelect = (item) => { |
| | | form.serviceType = item.value; |
| | | }; |
| | | |
| | | const onSalesOrderSelect = (item) => { |
| | | form.salesContractNo = item.name; |
| | | form.salesLedgerId = item.id; |
| | | setProductsFromSalesOrder(item, true); |
| | | }; |
| | | |
| | | const onUrgencySelect = (item) => { |
| | | form.urgency = item.value; |
| | | }; |
| | | |
| | | const normalizeProduct = (p) => { |
| | | return { |
| | | ...p, |
| | | id: p.id || p.productModelId || p.modelId, |
| | | productCategory: p.productCategory || p.productName || '', |
| | | specificationModel: p.specificationModel || p.model || '', |
| | | }; |
| | | }; |
| | | |
| | | const setProductsFromSalesOrder = (orderAction, selectAll = false) => { |
| | | const products = Array.isArray(orderAction?.products) ? orderAction.products : []; |
| | | const normalizedProducts = products.map(p => normalizeProduct(p)); |
| | | availableProducts.value = normalizedProducts; |
| | | if (selectAll) { |
| | | tableData.value = normalizedProducts.slice(); |
| | | return; |
| | | } |
| | | const ids = String(form.productModelIds || '') |
| | | .split(',') |
| | | .map(s => s.trim()) |
| | | .filter(Boolean); |
| | | if (ids.length === 0) { |
| | | tableData.value = normalizedProducts.slice(); |
| | | return; |
| | | } |
| | | const idSet = new Set(ids.map(String)); |
| | | tableData.value = normalizedProducts.filter(p => idSet.has(String(p.id))); |
| | | }; |
| | | |
| | | const fetchCustomers = () => { |
| | | getAllCustomerList({ current: 1, size: 1000 }).then(res => { |
| | | const records = res?.records ?? res?.data?.records ?? []; |
| | | customerList.value = Array.isArray(records) ? records : []; |
| | | }); |
| | | }; |
| | | |
| | | const fetchSalesOrders = (customerName) => { |
| | | getSalesLedger({ customerName }).then(res => { |
| | | const records = res?.records ?? res?.data?.records ?? []; |
| | | salesOrderList.value = Array.isArray(records) ? records : []; |
| | | if (form.salesContractNo) { |
| | | const match = salesOrderList.value.find(i => String(i.salesContractNo) === String(form.salesContractNo)); |
| | | if (match) { |
| | | setProductsFromSalesOrder({ id: match.id, name: match.salesContractNo, products: match.productData }, false); |
| | | form.salesLedgerId = match.id; |
| | | } |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | const removeProduct = (index) => { |
| | | tableData.value.splice(index, 1); |
| | | }; |
| | | |
| | | const isProductSelected = (product) => { |
| | | const id = product.id || product.productModelId || product.modelId; |
| | | return tableData.value.some(p => (p.id || p.productModelId || p.modelId) === id); |
| | | }; |
| | | |
| | | const toggleProduct = (product) => { |
| | | if (isReadonly.value) return; |
| | | const id = product.id || product.productModelId || product.modelId; |
| | | const index = tableData.value.findIndex(p => (p.id || p.productModelId || p.modelId) === id); |
| | | if (index > -1) { |
| | | tableData.value.splice(index, 1); |
| | | } else { |
| | | tableData.value.push(normalizeProduct(product)); |
| | | } |
| | | }; |
| | | |
| | | const submitForm = () => { |
| | | if (isReadonly.value) return; |
| | | formRef.value.validate().then(valid => { |
| | | if (valid) { |
| | | form.productModelIds = tableData.value.map(p => p.id || p.productModelId || p.modelId).join(','); |
| | | if (!form.checkUserId) { |
| | | form.checkUserId = userStore.id; |
| | | } |
| | | if (!form.feedbackDate) { |
| | | const now = new Date(); |
| | | form.feedbackDate = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`; |
| | | } |
| | | const api = operationType.value === 'add' ? afterSalesServiceAdd : afterSalesServiceUpdate; |
| | | |
| | | api(form).then((res) => { |
| | | if (res?.code === 200 || res?.code === undefined) { |
| | | uni.showToast({ title: operationType.value === 'add' ? 'æ°å¢æå' : 'ä¿®æ¹æå', icon: 'success' }); |
| | | setTimeout(() => goBack(), 500); |
| | | return; |
| | | } |
| | | uni.showToast({ title: res?.msg || 'æä½å¤±è´¥', icon: 'none' }); |
| | | }).catch(() => {}); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | operationType.value = uni.getStorageSync('afterSalesOperationType') || 'add'; |
| | | const editDataStr = uni.getStorageSync('afterSalesEditData'); |
| | | |
| | | fetchCustomers(); |
| | | |
| | | if (editDataStr) { |
| | | const editData = JSON.parse(editDataStr); |
| | | Object.assign(form, editData); |
| | | if (form.customerName) { |
| | | fetchSalesOrders(form.customerName); |
| | | } |
| | | } else { |
| | | form.checkUserId = userStore.id; |
| | | const now = new Date(); |
| | | form.feedbackDate = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`; |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .after-sales-edit { |
| | | min-height: 100vh; |
| | | background: #f8f9fa; |
| | | padding-bottom: 100px; |
| | | } |
| | | |
| | | .form-container { |
| | | background: #ffffff; |
| | | padding: 10px 20px; |
| | | margin-top: 10px; |
| | | } |
| | | |
| | | .product-section { |
| | | margin-top: 20px; |
| | | border-top: 1px solid #f0f0f0; |
| | | padding-top: 20px; |
| | | } |
| | | |
| | | .section-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | margin-bottom: 15px; |
| | | } |
| | | |
| | | .section-title { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: #333; |
| | | } |
| | | |
| | | .product-list { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 12px; |
| | | } |
| | | |
| | | .product-item { |
| | | background: #f8f9fa; |
| | | border-radius: 8px; |
| | | padding: 12px; |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | } |
| | | |
| | | .product-info { |
| | | flex: 1; |
| | | } |
| | | |
| | | .info-row { |
| | | display: flex; |
| | | margin-bottom: 4px; |
| | | font-size: 13px; |
| | | } |
| | | |
| | | .info-label { |
| | | color: #909399; |
| | | width: 70px; |
| | | } |
| | | |
| | | .info-value { |
| | | color: #303133; |
| | | flex: 1; |
| | | } |
| | | |
| | | .no-product { |
| | | text-align: center; |
| | | padding: 30px 0; |
| | | color: #999; |
| | | font-size: 14px; |
| | | } |
| | | |
| | | .product-select-popup { |
| | | height: 60vh; |
| | | display: flex; |
| | | flex-direction: column; |
| | | background: #fff; |
| | | } |
| | | |
| | | .popup-header { |
| | | padding: 15px 20px; |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | border-bottom: 1px solid #f0f0f0; |
| | | } |
| | | |
| | | .popup-title { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .product-scroll { |
| | | flex: 1; |
| | | padding: 10px 20px; |
| | | } |
| | | |
| | | .selectable-product { |
| | | display: flex; |
| | | align-items: center; |
| | | padding: 15px 0; |
| | | border-bottom: 1px solid #f5f5f5; |
| | | } |
| | | |
| | | .product-checkbox { |
| | | margin-right: 15px; |
| | | } |
| | | |
| | | .product-details { |
| | | display: flex; |
| | | flex-direction: column; |
| | | } |
| | | |
| | | .p-name { |
| | | font-size: 14px; |
| | | color: #333; |
| | | margin-bottom: 4px; |
| | | } |
| | | |
| | | .p-model { |
| | | font-size: 12px; |
| | | color: #999; |
| | | } |
| | | |
| | | .popup-footer { |
| | | padding: 20px; |
| | | border-top: 1px solid #f0f0f0; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="after-sales-registration"> |
| | | <!-- 使ç¨éç¨é¡µé¢å¤´é¨ç»ä»¶ --> |
| | | <PageHeader title="å®åç»è®°" @back="goBack" /> |
| | | |
| | | <!-- ç»è®¡å¡çåºå --> |
| | | <view class="stats-container"> |
| | | <view v-for="(item, index) in statsList" :key="index" class="stat-card"> |
| | | <view class="stat-icon" :style="{ backgroundColor: item.bgColor }"> |
| | | <up-icon :name="item.icon" :color="item.color" size="20"></up-icon> |
| | | </view> |
| | | <view class="stat-info"> |
| | | <text class="stat-number">{{ item.count }}</text> |
| | | <text class="stat-label">{{ item.label }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- æç´¢åçéåºå --> |
| | | <view class="search-section"> |
| | | <view class="search-bar"> |
| | | <view class="search-input"> |
| | | <up-input class="search-text" placeholder="请è¾å
¥å·¥åç¼å·æç´¢" v-model="searchForm.afterSalesServiceNo" @change="handleQuery" clearable /> |
| | | </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="tableData.length > 0"> |
| | | <view v-for="(item, index) in tableData" :key="index" class="ledger-item" @click="handleRowClick(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.afterSalesServiceNo }}</text> |
| | | </view> |
| | | <view class="item-tag"> |
| | | <up-tag :text="getStatusLabel(item.status)" :type="getStatusType(item.status)" size="mini"></up-tag> |
| | | </view> |
| | | </view> |
| | | <up-divider></up-divider> |
| | | <view class="item-details"> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">éå®åå·</text> |
| | | <text class="detail-value">{{ item.salesContractNo }}</text> |
| | | </view> |
| | | <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.feedbackDate }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">å®åç±»å</text> |
| | | <text class="detail-value">{{ getDictLabel(post_sale_waiting_list, item.serviceType) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">ç´§æ¥ç¨åº¦</text> |
| | | <text class="detail-value">{{ getDictLabel(degree_of_urgency, item.urgency) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">ç»è®°äºº</text> |
| | | <text class="detail-value">{{ item.checkNickName }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">é®é¢æè¿°</text> |
| | | <text class="detail-value">{{ item.disRes }}</text> |
| | | </view> |
| | | </view> |
| | | <up-divider v-if="canEdit(item)"></up-divider> |
| | | <view class="detail-buttons" v-if="canEdit(item)"> |
| | | <up-button size="small" type="primary" plain @click.stop="openForm('edit', item)">ç¼è¾</up-button> |
| | | <up-button size="small" type="error" plain @click.stop="handleDelete(item)">å é¤</up-button> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view v-else class="no-data"> |
| | | <text>ææ å®åç»è®°æ°æ®</text> |
| | | </view> |
| | | |
| | | <!-- æµ®å¨æä½æé® --> |
| | | <view class="fab-button" @click="openForm('add')"> |
| | | <up-icon name="plus" size="24" color="#ffffff"></up-icon> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, computed, onMounted } from 'vue'; |
| | | import { onShow } from '@dcloudio/uni-app'; |
| | | import { afterSalesServiceListPage, afterSalesServiceDelete, getSalesLedgerDetail } from '@/api/customerService/index'; |
| | | import useUserStore from '@/store/modules/user'; |
| | | import PageHeader from '@/components/PageHeader.vue'; |
| | | import { useDict } from '@/utils/dict'; |
| | | |
| | | const userStore = useUserStore(); |
| | | |
| | | // åå
¸ |
| | | const { post_sale_waiting_list, degree_of_urgency, work_order_status } = useDict( |
| | | 'post_sale_waiting_list', |
| | | 'degree_of_urgency', |
| | | 'work_order_status' |
| | | ); |
| | | |
| | | const statsList = ref([ |
| | | { |
| | | icon: 'file-text', |
| | | count: 0, |
| | | label: 'å
¨é¨å·¥å', |
| | | color: '#4080ff', |
| | | bgColor: '#eaf2ff' |
| | | }, |
| | | { |
| | | icon: 'file-text', |
| | | count: 0, |
| | | label: 'å·²å¤ç', |
| | | color: '#ff9a2e', |
| | | bgColor: '#fff5e6' |
| | | }, |
| | | { |
| | | icon: 'account', |
| | | count: 0, |
| | | label: '已宿', |
| | | color: '#00b42a', |
| | | bgColor: '#e6f7ed' |
| | | }, |
| | | ]); |
| | | |
| | | const searchForm = reactive({ |
| | | afterSalesServiceNo: '', |
| | | status: '', |
| | | urgency: '', |
| | | serviceType: '', |
| | | orderNo: '', |
| | | }); |
| | | |
| | | const tableData = ref([]); |
| | | const loading = ref(false); |
| | | const page = reactive({ |
| | | current: 1, |
| | | size: 20, |
| | | total: 0, |
| | | }); |
| | | |
| | | const goBack = () => { |
| | | uni.navigateBack(); |
| | | }; |
| | | |
| | | const handleQuery = () => { |
| | | page.current = 1; |
| | | getList(); |
| | | }; |
| | | |
| | | const getList = () => { |
| | | loading.value = true; |
| | | uni.showLoading({ title: 'å è½½ä¸...' }); |
| | | |
| | | getStats(); |
| | | |
| | | afterSalesServiceListPage({ ...searchForm, ...page }) |
| | | .then((res) => { |
| | | const records = res?.data?.records ?? res?.records ?? []; |
| | | const total = res?.data?.total ?? res?.total ?? 0; |
| | | tableData.value = Array.isArray(records) ? records : []; |
| | | page.total = Number.isFinite(Number(total)) ? Number(total) : 0; |
| | | }) |
| | | .finally(() => { |
| | | loading.value = false; |
| | | uni.hideLoading(); |
| | | }); |
| | | }; |
| | | |
| | | const getStats = () => { |
| | | getSalesLedgerDetail({}).then((res) => { |
| | | if (res.code === 200) { |
| | | const statsData = Array.isArray(res.data) ? res.data : []; |
| | | statsList.value[0].count = getStatsCountByStatus(statsData, 3); |
| | | statsList.value[1].count = getStatsCountByStatus(statsData, 2); |
| | | statsList.value[2].count = getStatsCountByStatus(statsData, 1); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | const getStatsCountByStatus = (list, status) => { |
| | | if (!Array.isArray(list)) return 0; |
| | | return list.find((item) => item?.status === status)?.count || 0; |
| | | }; |
| | | |
| | | const getStatusLabel = (status) => { |
| | | if (status === 1) return 'å¾
å¤ç'; |
| | | if (status === 2) return 'å·²å¤ç'; |
| | | return 'æªç¥'; |
| | | }; |
| | | |
| | | const getStatusType = (status) => { |
| | | if (status === 1) return 'error'; |
| | | if (status === 2) return 'success'; |
| | | return 'info'; |
| | | }; |
| | | |
| | | const getDictLabel = (dict, value) => { |
| | | if (!dict || !dict.value) return value; |
| | | const item = dict.value.find(i => i.value == value); |
| | | return item ? item.label : value; |
| | | }; |
| | | |
| | | const canEdit = (row) => { |
| | | if (!row) return false; |
| | | return row.status === 1 && String(row.checkUserId) === String(userStore.id); |
| | | } |
| | | |
| | | const handleRowClick = (row) => { |
| | | if (canEdit(row)) { |
| | | openForm('edit', row) |
| | | return |
| | | } |
| | | openForm('view', row) |
| | | } |
| | | |
| | | const openForm = (type, row) => { |
| | | uni.setStorageSync('afterSalesOperationType', type); |
| | | if (row) { |
| | | uni.setStorageSync('afterSalesEditData', JSON.stringify(row)); |
| | | } else { |
| | | uni.removeStorageSync('afterSalesEditData'); |
| | | } |
| | | uni.navigateTo({ |
| | | url: '/pages/customerService/feedbackRegistration/edit' |
| | | }); |
| | | }; |
| | | |
| | | const handleDelete = (row) => { |
| | | if (row.checkUserId !== userStore.id) { |
| | | uni.showToast({ title: 'ä¸å¯å é¤ä»äººç»´æ¤çæ°æ®', icon: 'none' }); |
| | | return; |
| | | } |
| | | |
| | | uni.showModal({ |
| | | title: 'æç¤º', |
| | | content: 'éä¸çå
容å°è¢«å é¤ï¼æ¯å¦ç¡®è®¤å é¤ï¼', |
| | | success: (res) => { |
| | | if (res.confirm) { |
| | | afterSalesServiceDelete([row.id]).then(() => { |
| | | uni.showToast({ title: 'å 餿å', icon: 'success' }); |
| | | getList(); |
| | | }); |
| | | } |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | onShow(() => { |
| | | getList(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/styles/sales-common.scss"; |
| | | |
| | | .after-sales-registration { |
| | | min-height: 100vh; |
| | | background: #f8f9fa; |
| | | } |
| | | |
| | | .stats-container { |
| | | display: flex; |
| | | gap: 10px; |
| | | padding: 15px 20px; |
| | | background: #ffffff; |
| | | } |
| | | |
| | | .stat-card { |
| | | flex: 1; |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | padding: 12px 10px; |
| | | background-color: #f8f9fa; |
| | | border-radius: 8px; |
| | | } |
| | | |
| | | .stat-icon { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | width: 36px; |
| | | height: 36px; |
| | | border-radius: 6px; |
| | | } |
| | | |
| | | .stat-info { |
| | | display: flex; |
| | | flex-direction: column; |
| | | } |
| | | |
| | | .stat-number { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | } |
| | | |
| | | .stat-label { |
| | | font-size: 10px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .detail-buttons { |
| | | display: flex; |
| | | gap: 10px; |
| | | justify-content: flex-end; |
| | | padding: 12px 0; |
| | | } |
| | | |
| | | .ledger-item { |
| | | padding: 0 16px; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="sales-account"> |
| | | <PageHeader :title="pageTitle" @back="goBack" /> |
| | | <view class="search-section"> |
| | | <up-form :model="form" :rules="rules" ref="formRef"> |
| | | <up-form-item label="æ¯åºæ¥æ" prop="expenseDate"> |
| | | <uni-datetime-picker type="date" v-model="form.expenseDate" /> |
| | | </up-form-item> |
| | | <up-form-item label="æ¯åºç±»å" prop="expenseType"> |
| | | <up-picker :columns="[expenseTypes]" key-name="label" v-model="expenseTypeIndex" @confirm="onExpenseTypeConfirm" /> |
| | | </up-form-item> |
| | | <up-form-item label="ä¾åºååç§°" prop="supplierName"> |
| | | <up-input v-model="form.supplierName" placeholder="请è¾å
¥" /> |
| | | </up-form-item> |
| | | <up-form-item label="æ¯åºéé¢" prop="expenseMoney"> |
| | | <up-input type="number" v-model="form.expenseMoney" placeholder="请è¾å
¥" /> |
| | | </up-form-item> |
| | | <up-form-item label="æ¯åºæè¿°" prop="expenseDescribed"> |
| | | <up-input v-model="form.expenseDescribed" placeholder="请è¾å
¥" /> |
| | | </up-form-item> |
| | | <up-form-item label="仿¬¾æ¹å¼" prop="expenseMethod"> |
| | | <up-picker :columns="[checkoutPayment]" key-name="label" v-model="expenseMethodIndex" @confirm="onMethodConfirm" /> |
| | | </up-form-item> |
| | | <up-form-item label="å票å·ç " prop="invoiceNumber"> |
| | | <up-input v-model="form.invoiceNumber" placeholder="请è¾å
¥" /> |
| | | </up-form-item> |
| | | <up-form-item label="夿³¨" prop="note"> |
| | | <up-textarea v-model="form.note" placeholder="请è¾å
¥" autoHeight /> |
| | | </up-form-item> |
| | | </up-form> |
| | | <view class="actions"> |
| | | <u-button type="primary" @click="submitForm">ä¿å</u-button> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, computed } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import { useDict } from "@/utils/dict"; |
| | | import { add, update, getAccountExpense } from "@/api/financialManagement/expenseManagement"; |
| | | |
| | | const operationType = ref("add"); |
| | | const id = ref(undefined); |
| | | |
| | | const { checkout_payment, expense_types } = useDict("checkout_payment", "expense_types"); |
| | | const checkoutPayment = ref([]); |
| | | const expenseTypes = ref([]); |
| | | const expenseTypeIndex = ref([0]); |
| | | const expenseMethodIndex = ref([0]); |
| | | |
| | | const formRef = ref(); |
| | | const form = reactive({ |
| | | expenseDate: undefined, |
| | | expenseType: undefined, |
| | | supplierName: "", |
| | | expenseMoney: undefined, |
| | | expenseDescribed: "", |
| | | expenseMethod: undefined, |
| | | invoiceNumber: "", |
| | | note: "", |
| | | }); |
| | | |
| | | const rules = { |
| | | expenseDate: [{ required: true, message: "è¯·éæ©", trigger: "change" }], |
| | | expenseType: [{ required: true, message: "è¯·éæ©", trigger: "change" }], |
| | | supplierName: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | expenseMoney: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | expenseDescribed: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | expenseMethod: [{ required: true, message: "è¯·éæ©", trigger: "change" }], |
| | | }; |
| | | |
| | | const pageTitle = computed(() => (operationType.value === "edit" ? "ç¼è¾æ¯åº" : "æ°å¢æ¯åº")); |
| | | |
| | | const onExpenseTypeConfirm = (e) => { |
| | | const item = expenseTypes.value[e.value[0]]; |
| | | if (item) form.expenseType = item.value; |
| | | }; |
| | | const onMethodConfirm = (e) => { |
| | | const item = checkoutPayment.value[e.value[0]]; |
| | | if (item) form.expenseMethod = item.value; |
| | | }; |
| | | |
| | | const syncDict = () => { |
| | | checkoutPayment.value = (checkout_payment?.value || []).map(i => ({ label: i.label, value: i.value })); |
| | | expenseTypes.value = (expense_types?.value || []).map(i => ({ label: i.label, value: i.value })); |
| | | }; |
| | | |
| | | const submitForm = () => { |
| | | formRef.value?.validate(async (valid) => { |
| | | if (!valid) return; |
| | | const payload = { ...form }; |
| | | const res = operationType.value === "edit" ? await update({ id: id.value, ...payload }) : await add(payload); |
| | | if (res?.code === 200) { |
| | | uni.navigateBack(); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | const goBack = () => { |
| | | uni.navigateBack(); |
| | | }; |
| | | |
| | | onLoad(async (query) => { |
| | | syncDict(); |
| | | operationType.value = query?.type || "add"; |
| | | if (query?.id) { |
| | | id.value = query.id; |
| | | const res = await getAccountExpense(id.value); |
| | | const data = res?.data ?? res; |
| | | Object.assign(form, data || {}); |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/styles/sales-common.scss"; |
| | | .actions { margin-top: 16px; } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="sales-account"> |
| | | <PageHeader title="æ¯åºç®¡ç" @back="goBack" /> |
| | | <view class="search-section"> |
| | | <view class="search-bar"> |
| | | <view class="search-input"> |
| | | <uni-datetime-picker type="daterange" v-model="filters.entryDate" @change="onDateChange" /> |
| | | </view> |
| | | <view class="search-input"> |
| | | <up-input readonly placeholder="仿¬¾æ¹å¼" v-model="expenseMethodLabel" @click="methodPickerShow = true" /> |
| | | </view> |
| | | <view class="filter-button" @click="getList"> |
| | | <up-icon name="search" size="24" color="#999" /> |
| | | </view> |
| | | </view> |
| | | <view class="actions"> |
| | | <u-button type="primary" size="small" @click="goAdd">æ°å¢</u-button> |
| | | </view> |
| | | </view> |
| | | <view class="ledger-list" v-if="list.length>0"> |
| | | <view class="ledger-item" v-for="item in list" :key="item.id"> |
| | | <view class="item-header"> |
| | | <view class="item-left"> |
| | | <view class="document-icon"><up-icon name="file-text" color="#fff" size="16" /></view> |
| | | <text class="item-id">{{ item.supplierName || '--' }}</text> |
| | | </view> |
| | | <view class="item-tag"> |
| | | <u-tag>{{ methodText(item.expenseMethod) }}</u-tag> |
| | | </view> |
| | | </view> |
| | | <up-divider></up-divider> |
| | | <view class="item-details"> |
| | | <view class="detail-row"><text class="detail-label">æ¯åºæ¥æ</text><text class="detail-value">{{ item.expenseDate || '--' }}</text></view> |
| | | <view class="detail-row"><text class="detail-label">æ¯åºç±»å</text><text class="detail-value">{{ expenseTypeText(item.expenseType) || '--' }}</text></view> |
| | | <view class="detail-row"><text class="detail-label">æ¯åºéé¢(å
)</text><text class="detail-value highlight">{{ fmtAmount(item.expenseMoney) }}</text></view> |
| | | <view class="detail-row"><text class="detail-label">å票å·ç </text><text class="detail-value">{{ item.invoiceNumber || '--' }}</text></view> |
| | | <view class="detail-row"><text class="detail-label">夿³¨</text><text class="detail-value">{{ item.note || '--' }}</text></view> |
| | | </view> |
| | | <view class="card-actions"> |
| | | <u-button size="small" @click="goEdit(item)" :disabled="!!item.businessId">ç¼è¾</u-button> |
| | | <u-button size="small" type="error" @click="confirmDelete(item)" :disabled="!!item.businessId">å é¤</u-button> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view class="no-data" v-else><text>ææ æ°æ®</text></view> |
| | | |
| | | <up-action-sheet :show="methodPickerShow" :actions="checkoutPayment" title="仿¬¾æ¹å¼" @select="onSelectMethod" @close="methodPickerShow=false" /> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive } from "vue"; |
| | | import { onShow } from "@dcloudio/uni-app"; |
| | | import { listPage, delAccountExpense } from "@/api/financialManagement/expenseManagement"; |
| | | import { useDict } from "@/utils/dict"; |
| | | |
| | | const list = ref([]); |
| | | const filters = reactive({ entryDate: null, expenseMethod: undefined, entryDateStart: undefined, entryDateEnd: undefined }); |
| | | const { checkout_payment, expense_types } = useDict("checkout_payment", "expense_types"); |
| | | const checkoutPayment = ref([]); |
| | | const expenseTypes = ref([]); |
| | | const methodPickerShow = ref(false); |
| | | const expenseMethodLabel = ref(""); |
| | | |
| | | const syncDict = () => { |
| | | checkoutPayment.value = (checkout_payment?.value || []).map(i => ({ label: i.label, value: i.value })); |
| | | expenseTypes.value = (expense_types?.value || []).map(i => ({ label: i.label, value: i.value })); |
| | | }; |
| | | |
| | | const getList = () => { |
| | | listPage({ expenseMethod: filters.expenseMethod, entryDateStart: filters.entryDateStart, entryDateEnd: filters.entryDateEnd, current: 1, size: 100 }) |
| | | .then(res => { |
| | | const records = res?.data?.records ?? res?.records ?? []; |
| | | list.value = records; |
| | | }); |
| | | }; |
| | | |
| | | const onDateChange = (val) => { |
| | | if (val && val.length === 2) { |
| | | filters.entryDateStart = val[0]; |
| | | filters.entryDateEnd = val[1]; |
| | | } else { |
| | | filters.entryDateStart = undefined; |
| | | filters.entryDateEnd = undefined; |
| | | } |
| | | }; |
| | | |
| | | const onSelectMethod = (e) => { |
| | | filters.expenseMethod = e.value; |
| | | expenseMethodLabel.value = e.label; |
| | | methodPickerShow.value = false; |
| | | }; |
| | | |
| | | const methodText = (v) => { |
| | | const m = checkoutPayment.value.find(i=>String(i.value)===String(v)); |
| | | return m?.label || "--"; |
| | | }; |
| | | const expenseTypeText = (v) => { |
| | | const m = expenseTypes.value.find(i=>String(i.value)===String(v)); |
| | | return m?.label || null; |
| | | }; |
| | | const fmtAmount = (v) => { |
| | | const n = parseFloat(v || 0); |
| | | return n.toFixed(2); |
| | | }; |
| | | |
| | | const goAdd = () => { |
| | | uni.navigateTo({ url: "/pages/financialManagement/expenseManagement/edit?type=add" }); |
| | | }; |
| | | const goEdit = (row) => { |
| | | uni.navigateTo({ url: `/pages/financialManagement/expenseManagement/edit?type=edit&id=${row.id}` }); |
| | | }; |
| | | const confirmDelete = (row) => { |
| | | uni.showModal({ |
| | | title: "æç¤º", |
| | | content: "确认å é¤è¯¥è®°å½ï¼", |
| | | success: async (r) => { |
| | | if (r.confirm) { |
| | | const ids = Array.isArray(row) ? row.map(i=>i.id) : [row.id]; |
| | | const res = await delAccountExpense(ids); |
| | | if (res?.code === 200) getList(); |
| | | } |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | const onExpenseTypeConfirm = (e) => { |
| | | const item = expenseTypes.value[e.value[0]]; |
| | | if (item) form.expenseType = item.value; |
| | | }; |
| | | const onMethodConfirm = (e) => { |
| | | const item = checkoutPayment.value[e.value[0]]; |
| | | if (item) form.expenseMethod = item.value; |
| | | }; |
| | | |
| | | const goBack = () => { |
| | | uni.navigateBack(); |
| | | }; |
| | | |
| | | syncDict(); |
| | | onShow(() => { |
| | | getList(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/styles/sales-common.scss"; |
| | | .actions { margin-top: 8px; } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="sales-account"> |
| | | <PageHeader :title="pageTitle" @back="goBack" /> |
| | | <view class="search-section"> |
| | | <up-form :model="form" :rules="rules" ref="formRef"> |
| | | <up-form-item label="忬¾äººå§å" prop="borrowerName"> |
| | | <up-input v-model="form.borrowerName" placeholder="请è¾å
¥" /> |
| | | </up-form-item> |
| | | <up-form-item label="忬¾éé¢(å
)" prop="borrowAmount"> |
| | | <up-input type="number" v-model="form.borrowAmount" placeholder="请è¾å
¥" /> |
| | | </up-form-item> |
| | | <up-form-item label="忬¾å©ç(%)" prop="interestRate"> |
| | | <up-input type="number" v-model="form.interestRate" placeholder="ä¾å¦ 5.85" /> |
| | | </up-form-item> |
| | | <up-form-item label="忬¾æ¥æ" prop="borrowDate"> |
| | | <uni-datetime-picker type="date" v-model="form.borrowDate" /> |
| | | </up-form-item> |
| | | <up-form-item v-if="operationType==='repay'" label="å®é
è¿æ¬¾æ¥æ" prop="repayDate"> |
| | | <uni-datetime-picker type="date" v-model="form.repayDate" /> |
| | | </up-form-item> |
| | | <up-form-item label="夿³¨" prop="remark"> |
| | | <up-textarea v-model="form.remark" autoHeight /> |
| | | </up-form-item> |
| | | </up-form> |
| | | <view class="actions"> |
| | | <u-button type="primary" @click="submitForm">ä¿å</u-button> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, computed } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import { add, update } from "@/api/financialManagement/loanManagement"; |
| | | |
| | | const operationType = ref("add"); |
| | | const id = ref(undefined); |
| | | |
| | | const formRef = ref(); |
| | | const form = reactive({ |
| | | borrowerName: "", |
| | | borrowAmount: undefined, |
| | | interestRate: undefined, |
| | | borrowDate: undefined, |
| | | repayDate: undefined, |
| | | remark: "", |
| | | }); |
| | | |
| | | const rules = { |
| | | borrowerName: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | borrowAmount: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | interestRate: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | borrowDate: [{ required: true, message: "è¯·éæ©", trigger: "change" }], |
| | | repayDate: [{ validator: (_r, v, cb) => { if (operationType.value==='repay' && !v) return cb(new Error('è¯·éæ©')); cb(); }, trigger: "change" }], |
| | | }; |
| | | |
| | | const pageTitle = computed(() => operationType.value==='repay' ? "è¿æ¬¾" : operationType.value==='edit' ? "ç¼è¾å款" : "æ°å¢å款"); |
| | | |
| | | const submitForm = () => { |
| | | formRef.value?.validate(async (valid) => { |
| | | if (!valid) return; |
| | | const payload = operationType.value==='repay' ? { ...form, status: 2 } : { ...form }; |
| | | const res = operationType.value==='add' ? await add(payload) : await update({ id: id.value, ...payload }); |
| | | if (res?.code === 200) { |
| | | uni.navigateBack(); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | const goBack = () => { |
| | | uni.navigateBack(); |
| | | }; |
| | | |
| | | onLoad((query) => { |
| | | operationType.value = query?.type || "add"; |
| | | if (query?.id) { |
| | | id.value = query.id; |
| | | // éè¿å¯¼èªåæ°æºå¸¦çåæ®µåå¡«ç± index 页å³å®ï¼è¿éä»
ä¿çæç®é»è¾ |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/styles/sales-common.scss"; |
| | | .actions { margin-top: 16px; } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="sales-account"> |
| | | <PageHeader title="忬¾ç®¡ç" @back="goBack" /> |
| | | <view class="search-section"> |
| | | <view class="search-bar"> |
| | | <view class="search-input"> |
| | | <up-input v-model="filters.borrowerName" placeholder="忬¾äººå§å" clearable /> |
| | | </view> |
| | | <view class="search-input"> |
| | | <uni-datetime-picker type="daterange" v-model="filters.borrowDate" @change="onDateChange" /> |
| | | </view> |
| | | <view class="search-input"> |
| | | <up-picker :columns="[statusOptions]" key-name="label" v-model="statusIndex" @confirm="onStatusConfirm"> |
| | | <up-input readonly :value="statusLabel" placeholder="忬¾ç¶æ" /> |
| | | </up-picker> |
| | | </view> |
| | | <view class="filter-button" @click="getList"> |
| | | <up-icon name="search" size="24" color="#999" /> |
| | | </view> |
| | | </view> |
| | | <view class="actions"> |
| | | <u-button type="primary" size="small" @click="goAdd">æ°å¢</u-button> |
| | | </view> |
| | | </view> |
| | | <view class="ledger-list" v-if="list.length>0"> |
| | | <view class="ledger-item" v-for="item in list" :key="item.id"> |
| | | <view class="item-header"> |
| | | <view class="item-left"> |
| | | <view class="document-icon"><up-icon name="file-text" color="#fff" size="16" /></view> |
| | | <text class="item-id">{{ item.borrowerName || '--' }}</text> |
| | | </view> |
| | | <view class="item-tag"> |
| | | <u-tag :type="statusType(item.status)">{{ statusText(item.status) }}</u-tag> |
| | | </view> |
| | | </view> |
| | | <up-divider></up-divider> |
| | | <view class="item-details"> |
| | | <view class="detail-row"><text class="detail-label">忬¾éé¢(å
)</text><text class="detail-value highlight">{{ fmtAmount(item.borrowAmount) }}</text></view> |
| | | <view class="detail-row"><text class="detail-label">忬¾å©ç(%)</text><text class="detail-value">{{ fmtRate(item.interestRate) }}</text></view> |
| | | <view class="detail-row"><text class="detail-label">忬¾æ¥æ</text><text class="detail-value">{{ item.borrowDate || '--' }}</text></view> |
| | | <view class="detail-row"><text class="detail-label">å®é
è¿æ¬¾æ¥æ</text><text class="detail-value">{{ item.repayDate || '--' }}</text></view> |
| | | <view class="detail-row"><text class="detail-label">夿³¨</text><text class="detail-value">{{ item.remark || '--' }}</text></view> |
| | | </view> |
| | | <view class="card-actions"> |
| | | <u-button size="small" @click="goEdit(item)">ç¼è¾</u-button> |
| | | <u-button size="small" type="warning" @click="goRepay(item)" :disabled="item.status!==1">è¿æ¬¾</u-button> |
| | | <u-button size="small" type="error" @click="confirmDelete(item)">å é¤</u-button> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view class="no-data" v-else><text>ææ æ°æ®</text></view> |
| | | |
| | | <up-popup :show="formShow" mode="bottom" @close="closeForm"> |
| | | <view class="popup"> |
| | | <view class="popup-header">{{ formMode==='add'?'æ°å¢å款': formMode==='repay'?'è¿æ¬¾':'ç¼è¾å款' }}</view> |
| | | <up-form :model="form" :rules="rules" ref="formRef"> |
| | | <up-form-item label="忬¾äººå§å" prop="borrowerName"> |
| | | <up-input v-model="form.borrowerName" placeholder="请è¾å
¥" /> |
| | | </up-form-item> |
| | | <up-form-item label="忬¾éé¢" prop="borrowAmount"> |
| | | <up-input type="number" v-model="form.borrowAmount" placeholder="请è¾å
¥" /> |
| | | </up-form-item> |
| | | <up-form-item label="忬¾å©ç(%)" prop="interestRate"> |
| | | <up-input type="number" v-model="form.interestRate" placeholder="ä¾å¦ 5.85" /> |
| | | </up-form-item> |
| | | <up-form-item label="忬¾æ¥æ" prop="borrowDate"> |
| | | <uni-datetime-picker type="date" v-model="form.borrowDate" /> |
| | | </up-form-item> |
| | | <up-form-item v-if="formMode==='repay'" label="å®é
è¿æ¬¾æ¥æ" prop="repayDate"> |
| | | <uni-datetime-picker type="date" v-model="form.repayDate" /> |
| | | </up-form-item> |
| | | <up-form-item label="夿³¨" prop="remark"> |
| | | <up-textarea v-model="form.remark" autoHeight /> |
| | | </up-form-item> |
| | | </up-form> |
| | | <view class="popup-actions"> |
| | | <u-button @click="closeForm">åæ¶</u-button> |
| | | <u-button type="primary" @click="submitForm">ä¿å</u-button> |
| | | </view> |
| | | </view> |
| | | </up-popup> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive } from "vue"; |
| | | import { onShow } from "@dcloudio/uni-app"; |
| | | import { listPage, delAccountLoan } from "@/api/financialManagement/loanManagement"; |
| | | |
| | | const list = ref([]); |
| | | const filters = reactive({ borrowerName: "", borrowDate: null, entryDateStart: undefined, entryDateEnd: undefined, status: undefined }); |
| | | const statusOptions = ref([{ label: "å
¨é¨", value: undefined }, { label: "å¾
è¿æ¬¾", value: 1 }, { label: "å·²è¿æ¬¾", value: 2 }]); |
| | | const statusIndex = ref([0]); |
| | | const statusLabel = ref(""); |
| | | |
| | | const formShow = ref(false); |
| | | const formMode = ref("add"); |
| | | const formRef = ref(); |
| | | const form = reactive({ |
| | | id: undefined, |
| | | borrowerName: "", |
| | | borrowAmount: undefined, |
| | | interestRate: undefined, |
| | | borrowDate: undefined, |
| | | repayDate: undefined, |
| | | remark: "", |
| | | status: undefined, |
| | | }); |
| | | const rules = { |
| | | borrowerName: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | borrowAmount: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | interestRate: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | borrowDate: [{ required: true, message: "è¯·éæ©", trigger: "change" }], |
| | | repayDate: [{ validator: (_r, v, cb)=>{ if (formMode.value==='repay' && !v) return cb(new Error('è¯·éæ©')); cb(); }, trigger: "change" }], |
| | | }; |
| | | |
| | | const getList = () => { |
| | | const extra = {}; |
| | | if (filters.entryDateStart && filters.entryDateEnd) { |
| | | extra.entryDateStart = filters.entryDateStart; |
| | | extra.entryDateEnd = filters.entryDateEnd; |
| | | } |
| | | if (filters.status) extra.status = filters.status; |
| | | listPage({ borrowerName: filters.borrowerName, ...extra, current: 1, size: 100 }) |
| | | .then(res => { |
| | | const records = res?.data?.records ?? res?.records ?? []; |
| | | list.value = records; |
| | | }); |
| | | }; |
| | | |
| | | const onDateChange = (val) => { |
| | | if (val && val.length === 2) { |
| | | filters.entryDateStart = val[0]; |
| | | filters.entryDateEnd = val[1]; |
| | | } else { |
| | | filters.entryDateStart = undefined; |
| | | filters.entryDateEnd = undefined; |
| | | } |
| | | }; |
| | | |
| | | const statusText = (s) => s===1?'å¾
è¿æ¬¾': s===2?'å·²è¿æ¬¾':''; |
| | | const statusType = (s) => s===1?'error': s===2?'success':'primary'; |
| | | const fmtAmount = (v) => { |
| | | const n = parseFloat(v || 0); |
| | | return n.toFixed(2); |
| | | }; |
| | | const fmtRate = (v) => { |
| | | if (v===undefined || v===null || v==='') return '-'; |
| | | const n = parseFloat(v); |
| | | return n.toFixed(2) + '%'; |
| | | }; |
| | | |
| | | const goAdd = () => { |
| | | uni.navigateTo({ url: "/pages/financialManagement/loanManagement/edit?type=add" }); |
| | | }; |
| | | const goEdit = (row) => { |
| | | uni.navigateTo({ url: `/pages/financialManagement/loanManagement/edit?type=edit&id=${row.id}` }); |
| | | }; |
| | | const goRepay = (row) => { |
| | | uni.navigateTo({ url: `/pages/financialManagement/loanManagement/edit?type=repay&id=${row.id}` }); |
| | | }; |
| | | const confirmDelete = (row) => { |
| | | uni.showModal({ |
| | | title: "æç¤º", |
| | | content: "确认å é¤è¯¥è®°å½ï¼", |
| | | success: async (r) => { |
| | | if (r.confirm) { |
| | | const ids = Array.isArray(row) ? row.map(i=>i.id) : [row.id]; |
| | | const res = await delAccountLoan(ids); |
| | | if (res?.code === 200) getList(); |
| | | } |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | const onStatusConfirm = (e) => { |
| | | const item = statusOptions.value[e.value[0]]; |
| | | if (item) { |
| | | filters.status = item.value; |
| | | statusLabel.value = item.label || ""; |
| | | } |
| | | }; |
| | | |
| | | const goBack = () => { |
| | | uni.navigateBack(); |
| | | }; |
| | | |
| | | onShow(() => { |
| | | getList(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/styles/sales-common.scss"; |
| | | .actions { margin-top: 8px; } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="sales-account"> |
| | | <PageHeader :title="pageTitle" @back="goBack" /> |
| | | <view class="search-section"> |
| | | <up-form :model="form" :rules="rules" ref="formRef"> |
| | | <up-form-item label="æ¶å
¥æ¥æ" prop="incomeDate"> |
| | | <uni-datetime-picker type="date" v-model="form.incomeDate" /> |
| | | </up-form-item> |
| | | <up-form-item label="æ¶å
¥ç±»å" prop="incomeType"> |
| | | <up-picker :columns="[incomeTypes]" key-name="label" v-model="incomeTypeIndex" @confirm="onIncomeTypeConfirm" /> |
| | | </up-form-item> |
| | | <up-form-item label="客æ·åç§°" prop="customerName"> |
| | | <up-input v-model="form.customerName" placeholder="请è¾å
¥" /> |
| | | </up-form-item> |
| | | <up-form-item label="æ¶å
¥éé¢" prop="incomeMoney"> |
| | | <up-input type="number" v-model="form.incomeMoney" placeholder="请è¾å
¥" /> |
| | | </up-form-item> |
| | | <up-form-item label="æ¶å
¥æè¿°" prop="incomeDescribed"> |
| | | <up-input v-model="form.incomeDescribed" placeholder="请è¾å
¥" /> |
| | | </up-form-item> |
| | | <up-form-item label="æ¶æ¬¾æ¹å¼" prop="incomeMethod"> |
| | | <up-picker :columns="[paymentMethods]" key-name="label" v-model="incomeMethodIndex" @confirm="onMethodConfirm" /> |
| | | </up-form-item> |
| | | <up-form-item label="å票å·ç " prop="invoiceNumber"> |
| | | <up-input v-model="form.invoiceNumber" placeholder="请è¾å
¥" /> |
| | | </up-form-item> |
| | | <up-form-item label="夿³¨" prop="note"> |
| | | <up-textarea v-model="form.note" placeholder="请è¾å
¥" autoHeight /> |
| | | </up-form-item> |
| | | </up-form> |
| | | <view class="actions"> |
| | | <u-button type="primary" @click="submitForm">ä¿å</u-button> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, computed } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import { useDict } from "@/utils/dict"; |
| | | import { add, update, getAccountIncome } from "@/api/financialManagement/revenueManagement"; |
| | | |
| | | const operationType = ref("add"); |
| | | const id = ref(undefined); |
| | | |
| | | const { payment_methods, income_types } = useDict("payment_methods", "income_types"); |
| | | const paymentMethods = ref([]); |
| | | const incomeTypes = ref([]); |
| | | const incomeTypeIndex = ref([0]); |
| | | const incomeMethodIndex = ref([0]); |
| | | |
| | | const formRef = ref(); |
| | | const form = reactive({ |
| | | incomeDate: undefined, |
| | | incomeType: undefined, |
| | | customerName: "", |
| | | incomeMoney: undefined, |
| | | incomeDescribed: "", |
| | | incomeMethod: undefined, |
| | | invoiceNumber: "", |
| | | note: "", |
| | | }); |
| | | |
| | | const rules = { |
| | | incomeDate: [{ required: true, message: "è¯·éæ©", trigger: "change" }], |
| | | incomeType: [{ required: true, message: "è¯·éæ©", trigger: "change" }], |
| | | customerName: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | incomeMoney: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | incomeDescribed: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | incomeMethod: [{ required: true, message: "è¯·éæ©", trigger: "change" }], |
| | | }; |
| | | |
| | | const pageTitle = computed(() => (operationType.value === "edit" ? "ç¼è¾æ¶å
¥" : "æ°å¢æ¶å
¥")); |
| | | |
| | | const onIncomeTypeConfirm = (e) => { |
| | | const item = incomeTypes.value[e.value[0]]; |
| | | if (item) form.incomeType = item.value; |
| | | }; |
| | | const onMethodConfirm = (e) => { |
| | | const item = paymentMethods.value[e.value[0]]; |
| | | if (item) form.incomeMethod = item.value; |
| | | }; |
| | | |
| | | const syncDict = () => { |
| | | paymentMethods.value = (payment_methods?.value || []).map(i => ({ label: i.label, value: i.value })); |
| | | incomeTypes.value = (income_types?.value || []).filter(i=>i.value!=3).map(i => ({ label: i.label, value: i.value })); |
| | | }; |
| | | |
| | | const submitForm = () => { |
| | | formRef.value?.validate(async (valid) => { |
| | | if (!valid) return; |
| | | const payload = { ...form }; |
| | | const res = operationType.value === "edit" ? await update({ id: id.value, ...payload }) : await add(payload); |
| | | if (res?.code === 200) { |
| | | uni.navigateBack(); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | const goBack = () => { |
| | | uni.navigateBack(); |
| | | }; |
| | | |
| | | onLoad(async (query) => { |
| | | syncDict(); |
| | | operationType.value = query?.type || "add"; |
| | | if (query?.id) { |
| | | id.value = query.id; |
| | | const res = await getAccountIncome(id.value); |
| | | const data = res?.data ?? res; |
| | | Object.assign(form, data || {}); |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/styles/sales-common.scss"; |
| | | .actions { margin-top: 16px; } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="sales-account"> |
| | | <PageHeader title="æ¶å
¥ç®¡ç" @back="goBack" /> |
| | | <view class="search-section"> |
| | | <view class="search-bar"> |
| | | <view class="search-input"> |
| | | <uni-datetime-picker type="daterange" v-model="filters.entryDate" @change="onDateChange" /> |
| | | </view> |
| | | <view class="search-input"> |
| | | <up-input readonly placeholder="æ¶æ¬¾æ¹å¼" v-model="incomeMethodLabel" @click="methodPickerShow = true" /> |
| | | </view> |
| | | <view class="filter-button" @click="getList"> |
| | | <up-icon name="search" size="24" color="#999" /> |
| | | </view> |
| | | </view> |
| | | <view class="actions"> |
| | | <u-button type="primary" size="small" @click="goAdd">æ°å¢</u-button> |
| | | </view> |
| | | </view> |
| | | <view class="ledger-list" v-if="list.length>0"> |
| | | <view class="ledger-item" v-for="item in list" :key="item.id"> |
| | | <view class="item-header"> |
| | | <view class="item-left"> |
| | | <view class="document-icon"><up-icon name="file-text" color="#fff" size="16" /></view> |
| | | <text class="item-id">{{ item.customerName || '--' }}</text> |
| | | </view> |
| | | <view class="item-tag"> |
| | | <u-tag>{{ methodText(item.incomeMethod) }}</u-tag> |
| | | </view> |
| | | </view> |
| | | <up-divider></up-divider> |
| | | <view class="item-details"> |
| | | <view class="detail-row"><text class="detail-label">æ¶å
¥æ¥æ</text><text class="detail-value">{{ item.incomeDate || '--' }}</text></view> |
| | | <view class="detail-row"><text class="detail-label">æ¶å
¥ç±»å</text><text class="detail-value">{{ incomeTypeText(item.incomeType) || '--' }}</text></view> |
| | | <view class="detail-row"><text class="detail-label">æ¶å
¥éé¢(å
)</text><text class="detail-value highlight">{{ fmtAmount(item.incomeMoney) }}</text></view> |
| | | <view class="detail-row"><text class="detail-label">å票å·ç </text><text class="detail-value">{{ item.invoiceNumber || '--' }}</text></view> |
| | | <view class="detail-row"><text class="detail-label">夿³¨</text><text class="detail-value">{{ item.note || '--' }}</text></view> |
| | | </view> |
| | | <view class="card-actions"> |
| | | <u-button size="small" @click="goEdit(item)" :disabled="!!item.businessId">ç¼è¾</u-button> |
| | | <u-button size="small" type="error" @click="confirmDelete(item)" :disabled="!!item.businessId">å é¤</u-button> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view class="no-data" v-else><text>ææ æ°æ®</text></view> |
| | | |
| | | <up-action-sheet :show="methodPickerShow" :actions="paymentMethods" title="æ¶æ¬¾æ¹å¼" @select="onSelectMethod" @close="methodPickerShow=false" /> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive } from "vue"; |
| | | import { onShow } from "@dcloudio/uni-app"; |
| | | import { listPage, delAccountIncome } from "@/api/financialManagement/revenueManagement"; |
| | | import { useDict } from "@/utils/dict"; |
| | | |
| | | const list = ref([]); |
| | | const filters = reactive({ entryDate: null, incomeMethod: undefined, entryDateStart: undefined, entryDateEnd: undefined }); |
| | | const { payment_methods, income_types } = useDict("payment_methods", "income_types"); |
| | | const paymentMethods = ref([]); |
| | | const incomeTypes = ref([]); |
| | | const methodPickerShow = ref(false); |
| | | const incomeMethodLabel = ref(""); |
| | | |
| | | const syncDict = () => { |
| | | paymentMethods.value = (payment_methods?.value || []).map(i => ({ label: i.label, value: i.value })); |
| | | incomeTypes.value = (income_types?.value || []).filter(i=>i.value!=3).map(i => ({ label: i.label, value: i.value })); |
| | | }; |
| | | |
| | | const getList = () => { |
| | | listPage({ incomeMethod: filters.incomeMethod, entryDateStart: filters.entryDateStart, entryDateEnd: filters.entryDateEnd, current: 1, size: 100 }) |
| | | .then(res => { |
| | | const records = res?.data?.records ?? res?.records ?? []; |
| | | list.value = records; |
| | | }); |
| | | }; |
| | | |
| | | const onDateChange = (val) => { |
| | | if (val && val.length === 2) { |
| | | filters.entryDateStart = val[0]; |
| | | filters.entryDateEnd = val[1]; |
| | | } else { |
| | | filters.entryDateStart = undefined; |
| | | filters.entryDateEnd = undefined; |
| | | } |
| | | }; |
| | | |
| | | const onSelectMethod = (e) => { |
| | | filters.incomeMethod = e.value; |
| | | incomeMethodLabel.value = e.label; |
| | | methodPickerShow.value = false; |
| | | }; |
| | | |
| | | const methodText = (v) => { |
| | | const m = paymentMethods.value.find(i=>String(i.value)===String(v)); |
| | | return m?.label || "--"; |
| | | }; |
| | | const incomeTypeText = (v) => { |
| | | const m = incomeTypes.value.find(i=>String(i.value)===String(v)); |
| | | return m?.label || null; |
| | | }; |
| | | const fmtAmount = (v) => { |
| | | const n = parseFloat(v || 0); |
| | | return n.toFixed(2); |
| | | }; |
| | | |
| | | const goAdd = () => { |
| | | uni.navigateTo({ url: "/pages/financialManagement/revenueManagement/edit?type=add" }); |
| | | }; |
| | | const goEdit = (row) => { |
| | | uni.navigateTo({ url: `/pages/financialManagement/revenueManagement/edit?type=edit&id=${row.id}` }); |
| | | }; |
| | | const confirmDelete = (row) => { |
| | | uni.showModal({ |
| | | title: "æç¤º", |
| | | content: "确认å é¤è¯¥è®°å½ï¼", |
| | | success: async (r) => { |
| | | if (r.confirm) { |
| | | const ids = Array.isArray(row) ? row.map(i=>i.id) : [row.id]; |
| | | const res = await delAccountIncome(ids); |
| | | if (res?.code === 200) getList(); |
| | | } |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | const onIncomeTypeConfirm = (e) => { |
| | | const item = incomeTypes.value[e.value[0]]; |
| | | if (item) form.incomeType = item.value; |
| | | }; |
| | | const onMethodConfirm = (e) => { |
| | | const item = paymentMethods.value[e.value[0]]; |
| | | if (item) form.incomeMethod = item.value; |
| | | }; |
| | | |
| | | const goBack = () => { |
| | | uni.navigateBack(); |
| | | }; |
| | | |
| | | syncDict(); |
| | | onShow(() => { |
| | | getList(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/styles/sales-common.scss"; |
| | | .actions { margin-top: 8px; } |
| | | </style> |
| | |
| | | import {getStaffJoinInfo, staffJoinAdd, staffJoinUpdate} from "@/api/personnelManagement/onboarding.js"; |
| | | import {userListNoPageByTenantId} from "@/api/system/user.js"; |
| | | import {productionDispatch} from "@/api/productionManagement/productionOrder.js"; |
| | | import useUserStore from "@/store/modules/user.js"; |
| | | import useUserStore from "@/store/modules/user"; |
| | | import dayjs from "dayjs"; |
| | | const { proxy } = getCurrentInstance() |
| | | const emit = defineEmits(['close']) |
| | |
| | | icon: "/static/images/icon/caigouguanli.svg", |
| | | label: "éè´éè´§", |
| | | }, |
| | | { |
| | | icon: "/static/images/icon/gongchuguanli.svg", |
| | | label: "ä¾åºåæ¡£æ¡", |
| | | }, |
| | | ]); |
| | | |
| | | // è´¢å¡ç®¡çåè½æ°æ® |
| | |
| | | icon: "/static/images/icon/fukuanliushui.svg", |
| | | label: "仿¬¾æµæ°´", |
| | | }, |
| | | { |
| | | icon: "/static/images/icon/huikuandengji.svg", |
| | | label: "æ¶å
¥ç®¡ç", |
| | | }, |
| | | { |
| | | icon: "/static/images/icon/fukuandengji.svg", |
| | | label: "æ¯åºç®¡ç", |
| | | }, |
| | | { |
| | | icon: "/static/images/icon/huikuanliushui.svg", |
| | | label: "忬¾ç®¡ç", |
| | | }, |
| | | ]); |
| | | |
| | | // æ¡£æ¡ç®¡çåè½æ°æ® |
| | | const archiveManagementItems = reactive([ |
| | | { |
| | | icon: "/static/images/icon/gongchuguanli.svg", |
| | | label: "ä¾åºåæ¡£æ¡", |
| | | }, |
| | | ]); |
| | | |
| | | // å®åæå¡åè½æ°æ® |
| | | const afterSalesServiceItems = reactive([ |
| | | { |
| | | icon: "/static/images/icon/xiaoshoutaizhang.svg", |
| | | label: "åé¦ç»è®°", |
| | | }, |
| | | { |
| | | icon: "/static/images/icon/caigouguanli.svg", |
| | | label: "å®åå¤ç", |
| | | }, |
| | | ]); |
| | | |
| | | const humanResourcesItems = reactive([ |
| | |
| | | case "仿¬¾æµæ°´": |
| | | uni.navigateTo({ |
| | | url: "/pages/procurementManagement/receiptPaymentHistory/index", |
| | | }); |
| | | break; |
| | | case "æ¶å
¥ç®¡ç": |
| | | uni.navigateTo({ |
| | | url: "/pages/financialManagement/revenueManagement/index", |
| | | }); |
| | | break; |
| | | case "æ¯åºç®¡ç": |
| | | uni.navigateTo({ |
| | | url: "/pages/financialManagement/expenseManagement/index", |
| | | }); |
| | | break; |
| | | case "忬¾ç®¡ç": |
| | | uni.navigateTo({ |
| | | url: "/pages/financialManagement/loanManagement/index", |
| | | }); |
| | | break; |
| | | case "ä¾åºå徿¥": |
| | |
| | | url: "/pages/qualityManagement/finalInspection/index", |
| | | }); |
| | | break; |
| | | case "åé¦ç»è®°": |
| | | uni.navigateTo({ |
| | | url: "/pages/customerService/feedbackRegistration/index", |
| | | }); |
| | | break; |
| | | case "å®åå¤ç": |
| | | uni.navigateTo({ |
| | | url: "/pages/customerService/afterSalesHandling/index", |
| | | }); |
| | | break; |
| | | default: |
| | | uni.showToast({ |
| | | title: `ç¹å»äº${item.label}`, |
| | |
| | | // å®ä¹èåé
ç½®æ å° |
| | | const menuMapping = { |
| | | collaboration: { target: collaborationItems, specialMapping: { "è§ç« å¶åº¦": "è§ç« å¶åº¦ç®¡ç" } }, |
| | | archiveManagement: { target: archiveManagementItems, specialMapping: { "ä¾åºåæ¡£æ¡": "ä¾åºå管ç" } }, |
| | | }; |
| | | console.log(allowedMenuTitles) |
| | | // éç¨è¿æ»¤å½æ° |
| | |
| | | filterArray(marketingItems); |
| | | filterArray(purchaseItems); |
| | | filterArray(financeManagementItems); |
| | | filterArray(archiveManagementItems); |
| | | filterArray(afterSalesServiceItems); |
| | | filterArray(archiveManagementItems, menuMapping.archiveManagement.specialMapping); |
| | | filterArray(collaborationItems, menuMapping.collaboration.specialMapping); |
| | | filterArray(safetyItems); |
| | | filterArray(humanResourcesItems); |
| | |
| | | const hasMarketingItems = computed(() => marketingItems.length > 0); |
| | | const hasPurchaseItems = computed(() => purchaseItems.length > 0); |
| | | const hasFinanceManagementItems = computed(() => financeManagementItems.length > 0); |
| | | const hasArchiveManagementItems = computed(() => true); |
| | | const hasAfterSalesServiceItems = computed(() => true); |
| | | const hasArchiveManagementItems = computed(() => archiveManagementItems.length > 0); |
| | | const hasAfterSalesServiceItems = computed(() => afterSalesServiceItems.length > 0); |
| | | const hasCollaborationItems = computed(() => collaborationItems.length > 0); |
| | | const hasSafetyItems = computed(() => safetyItems.length > 0); |
| | | const hasQualityItems = computed(() => qualityItems.length > 0); |