ZN
7 天以前 dd630fede0cc46500fe898c75464e3e04ce82b0f
feat(财务与售后): 新增财务管理与售后管理模块

- 新增财务管理模块:收入管理、支出管理、借款管理的API接口和页面
- 新增售后管理模块:售后登记、售后处理、附件管理的完整功能
- 更新工作台菜单配置,添加财务和售后相关菜单项
- 修复生产调度表单中用户store导入路径问题
已添加15个文件
已修改3个文件
2564 ■■■■■ 文件已修改
src/api/customerService/index.js 93 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/financialManagement/expenseManagement.js 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/financialManagement/loanManagement.js 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/financialManagement/revenueManagement.js 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages.json 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/customerService/afterSalesHandling/fileList.vue 300 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/customerService/afterSalesHandling/handle.vue 121 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/customerService/afterSalesHandling/index.vue 181 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/customerService/feedbackRegistration/edit.vue 472 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/customerService/feedbackRegistration/index.vue 323 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/financialManagement/expenseManagement/edit.vue 120 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/financialManagement/expenseManagement/index.vue 149 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/financialManagement/loanManagement/edit.vue 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/financialManagement/loanManagement/index.vue 196 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/financialManagement/revenueManagement/edit.vue 120 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/financialManagement/revenueManagement/index.vue 149 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/productionManagement/productionDispatching/components/formDia.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/works.vue 61 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/customerService/index.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,93 @@
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,
    })
}
src/api/financialManagement/expenseManagement.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,40 @@
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",
  });
};
src/api/financialManagement/loanManagement.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
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,
  });
};
src/api/financialManagement/revenueManagement.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,40 @@
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",
  });
};
src/pages.json
@@ -19,6 +19,48 @@
      }
    },
    {
      "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": "首页",
@@ -999,6 +1041,41 @@
      }
    },
    {
      "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": "出厂检验添加",
src/pages/customerService/afterSalesHandling/fileList.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,300 @@
<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>
src/pages/customerService/afterSalesHandling/handle.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,121 @@
<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>
src/pages/customerService/afterSalesHandling/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,181 @@
<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>
src/pages/customerService/feedbackRegistration/edit.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,472 @@
<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>
src/pages/customerService/feedbackRegistration/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,323 @@
<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>
src/pages/financialManagement/expenseManagement/edit.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,120 @@
<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>
src/pages/financialManagement/expenseManagement/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,149 @@
<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>
src/pages/financialManagement/loanManagement/edit.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,87 @@
<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>
src/pages/financialManagement/loanManagement/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,196 @@
<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>
src/pages/financialManagement/revenueManagement/edit.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,120 @@
<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>
src/pages/financialManagement/revenueManagement/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,149 @@
<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>
src/pages/productionManagement/productionDispatching/components/formDia.vue
@@ -94,7 +94,7 @@
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'])
src/pages/works.vue
@@ -301,10 +301,6 @@
      icon: "/static/images/icon/caigouguanli.svg",
      label: "采购退货",
    },
    {
      icon: "/static/images/icon/gongchuguanli.svg",
      label: "供应商档案",
    },
  ]);
  // è´¢åŠ¡ç®¡ç†åŠŸèƒ½æ•°æ®
@@ -341,14 +337,38 @@
      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([
@@ -550,6 +570,21 @@
      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 "供应商往来":
@@ -810,6 +845,16 @@
          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}`,
@@ -998,6 +1043,7 @@
    // å®šä¹‰èœå•配置映射
    const menuMapping = {
      collaboration: { target: collaborationItems, specialMapping: { "规章制度": "规章制度管理" } },
      archiveManagement: { target: archiveManagementItems, specialMapping: { "供应商档案": "供应商管理" } },
    };
    console.log(allowedMenuTitles)
    // é€šç”¨è¿‡æ»¤å‡½æ•°
@@ -1016,8 +1062,7 @@
    filterArray(marketingItems);
    filterArray(purchaseItems);
    filterArray(financeManagementItems);
    filterArray(archiveManagementItems);
    filterArray(afterSalesServiceItems);
    filterArray(archiveManagementItems, menuMapping.archiveManagement.specialMapping);
    filterArray(collaborationItems, menuMapping.collaboration.specialMapping);
    filterArray(safetyItems);
    filterArray(humanResourcesItems);
@@ -1030,8 +1075,8 @@
  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);