Commit fb677d76ef6fcf727571c966fdaf5826804005dd

Authored by chenhang4442024
1 parent c6c33024

feat: 问题列表

fix: 禁止业务员对生产科,数量进行字段审核。
src/api/project/quest.ts
... ... @@ -5,28 +5,40 @@ enum Api {
5 5 QUEST_CREATE = '/order/erp/quest/add',
6 6 QUEST_DELETE = '/order/erp/quest/delete_by_id',
7 7 QUEST_UPDATE = '/order/erp/quest/edit',
  8 + QUEST_SET_STATUS = '/order/erp/quest/setStatus',
  9 + QUEST_RELEASE_LIST = '/order/erp/quest/realseList',
  10 + QUEST_GET_ALL = '/order/erp/quest/get_all',
  11 + QUEST_QUERY_TITLE = '/order/erp/quest/queryTitle',
8 12 }
9 13  
10 14 export const questUpdate=async(data:any)=>{
11 15 const res=await defHttp.post<any>({
12 16 url: Api.QUEST_UPDATE,
13   - data},{message:'操作成功'});
  17 + data,
  18 + successMsg: '操作成功'
  19 + });
14 20 return res;
15 21 }
16 22  
17 23 export const questDelete = async (ids: number[]) => {
18   - console.log('ids',ids);
19   - const res = await defHttp.post<any>({ url: Api.QUEST_DELETE, data: {ids} }, { message: '删除成功' });
  24 + const res = await defHttp.post<any>({
  25 + url: Api.QUEST_DELETE,
  26 + data: {ids},
  27 + successMsg: '删除成功'
  28 + });
20 29 return res;
21 30 };
22 31  
23 32 export const questCreate = async (data: any) => {
24   - const res = await defHttp.post<any>({ url: Api.QUEST_CREATE, data }, { message: '创建成功' });
  33 + const res = await defHttp.post<any>({
  34 + url: Api.QUEST_CREATE,
  35 + data,
  36 + successMsg: '创建成功'
  37 + });
25 38 return res;
26 39 };
27 40  
28 41 export const getQuestList = async (params: any) => {
29   - console.log('params',params);
30 42 const res=await defHttp.post<any>({
31 43 url: Api.QUEST_LIST,
32 44 params,
... ... @@ -37,3 +49,53 @@ export const getQuestList = async (params: any) =&gt; {
37 49 };
38 50 return result;
39 51 };
  52 +
  53 +export const setQuestStatus = async (data: { id: number; status: number; refuseRemark?: string }) => {
  54 + const res = await defHttp.post<any>({
  55 + url: Api.QUEST_SET_STATUS,
  56 + data,
  57 + successMsg: data.status === 10 ? '审核通过成功' : '审核驳回成功'
  58 + });
  59 + return res;
  60 +};
  61 +
  62 +export const getQuestReleaseData = async (id: number) => {
  63 + const res = await defHttp.post<any>({
  64 + url: Api.QUEST_RELEASE_LIST,
  65 + data: { id }
  66 + });
  67 +
  68 + // 如果返回的是带记录和总数的格式,处理为标准格式
  69 + if (res && res.records && res.total !== undefined) {
  70 + return {
  71 + items: res.records,
  72 + total: res.total
  73 + };
  74 + }
  75 +
  76 + return res;
  77 +};
  78 +
  79 +/**
  80 + * 获取所有扣款原因列表
  81 + * @returns 扣款原因列表数组,包含id和title
  82 + */
  83 +export const getAllQuests = async () => {
  84 + const res = await defHttp.post<any>({
  85 + url: Api.QUEST_GET_ALL
  86 + });
  87 + return res;
  88 +};
  89 +
  90 +/**
  91 + * 根据输入的关键词搜索匹配的扣款原因
  92 + * @param title 搜索关键词
  93 + * @returns 匹配的扣款原因列表数组,包含id和title
  94 + */
  95 +export const queryQuestTitle = async (title: string) => {
  96 + const res = await defHttp.post<any>({
  97 + url: Api.QUEST_QUERY_TITLE,
  98 + data: { title }
  99 + });
  100 + return res;
  101 +};
... ...
src/views/project/finance/financeList/FinanceEdit.vue
... ... @@ -16,16 +16,16 @@
16 16 <BasicForm @register="registerForm" />
17 17 </div> -->
18 18 <div style="font-size: 15px">实际收款金额1$</div>
19   - <a-input v-model:value="input1" placeholder="请输入" :disabled="status === 10" auto-size />
  19 + <a-input v-model:value="input1" placeholder="请输入" :disabled="status === 10 || status === 40" auto-size />
20 20 <div style="margin: 16px 0"></div>
21 21 <div style="font-size: 15px">实际收款金额2$</div>
22   - <a-input v-model:value="input2" placeholder="请输入" :disabled="status === 10" auto-size />
  22 + <a-input v-model:value="input2" placeholder="请输入" :disabled="status === 10 || status === 40" auto-size />
23 23 <div style="margin: 16px 0"></div>
24 24 <div style="font-size: 15px">实际收款金额3$</div>
25   - <a-input v-model:value="input3" placeholder="请输入" :disabled="status === 10" auto-size />
  25 + <a-input v-model:value="input3" placeholder="请输入" :disabled="status === 10 || status === 40" auto-size />
26 26 <div style="margin: 16px 0"></div>
27 27 <div style="font-size: 15px">其他费用金额$</div>
28   - <a-input v-model:value="input4" placeholder="请输入" :disabled="status === 10" auto-size />
  28 + <a-input v-model:value="input4" placeholder="请输入" :disabled="status === 10 || status === 40" auto-size />
29 29 <div style="margin: 16px 0"></div>
30 30  
31 31 <!-- <template #titleToolbar> <a-button type="primary"> 申请编辑权限 </a-button></template> -->
... ...
src/views/project/finance/financeList/FinanceEditCheck.vue
... ... @@ -16,13 +16,13 @@
16 16 <BasicForm @register="registerForm" />
17 17 </div> -->
18 18 <div style="font-size: 15px">实际付款金额1¥</div>
19   - <a-input v-model:value="input1" placeholder="请输入" :disabled="status === 10" auto-size />
  19 + <a-input v-model:value="input1" placeholder="请输入" :disabled="status === 10 || status === 40" auto-size />
20 20 <div style="margin: 16px 0"></div>
21 21 <div style="font-size: 15px">实际付款金额2¥</div>
22   - <a-input v-model:value="input2" placeholder="请输入" :disabled="status === 10" auto-size />
  22 + <a-input v-model:value="input2" placeholder="请输入" :disabled="status === 10 || status === 40" auto-size />
23 23 <div style="margin: 16px 0"></div>
24 24 <div style="font-size: 15px">实际付款金额3¥</div>
25   - <a-input v-model:value="input3" placeholder="请输入" :disabled="status === 10" auto-size />
  25 + <a-input v-model:value="input3" placeholder="请输入" :disabled="status === 10 || status === 40" auto-size />
26 26 <div style="margin: 16px 0"></div>
27 27  
28 28 <!-- <template #titleToolbar> <a-button type="primary"> 申请编辑权限 </a-button></template> -->
... ...
src/views/project/finance/financeList/TrackEdit.vue
... ... @@ -15,7 +15,16 @@
15 15 >
16 16 <div>
17 17 <div style="font-size: 15px">客户扣款金额$</div>
18   - <a-input v-model:value="input1" placeholder="请输入" :disabled="status === 10" auto-size />
  18 + <a-input
  19 + v-model:value="input1"
  20 + placeholder="请输入"
  21 + :disabled="status === 10 || status === 40"
  22 + auto-size
  23 + :status="amountMismatch ? 'error' : ''"
  24 + />
  25 + <div v-if="amountMismatch" style="color: #ff4d4f; font-size: 19px; margin-top: 4px">
  26 + 金额不匹配!扣款原因金额为: {{ bindedDeductAmount }}
  27 + </div>
19 28 <div style="margin: 16px 0"></div>
20 29 <div>上传扣款单</div
21 30 ><a-space direction="vertical" style="width: 100%" size="large">
... ... @@ -26,11 +35,45 @@
26 35 :max-count="1"
27 36 :action="updateDeductUrl"
28 37 @change="handleChange"
29   - :disabled="status === 10"
  38 + :disabled="status === 10 || status === 40"
30 39 >
31 40 <a-button> 上传扣款单 </a-button>
32 41 </a-upload>
33 42 </a-space>
  43 +
  44 + <div style="margin: 16px 0"></div>
  45 + <div>扣款原因(可选)</div>
  46 + <a-select
  47 + v-model:value="selectedTitle"
  48 + placeholder="请选择或输入扣款原因"
  49 + style="width: 100%"
  50 + :filter-option="false"
  51 + show-search
  52 + :disabled="status === 10"
  53 + @search="handleSearch"
  54 + @change="handleTitleChange"
  55 + :loading="titleLoading"
  56 + :options="formattedOptions"
  57 + :not-found-content="titleLoading ? '加载中...' : (titleOptions.length === 0 ? '未找到匹配项' : null)"
  58 + allow-clear
  59 + @clear="handleClear"
  60 + :auto-focus="true"
  61 + :default-open="false"
  62 + :drop-down-style="{ maxHeight: '400px', overflow: 'auto' }"
  63 + >
  64 + </a-select>
  65 + <!-- 调试信息 -->
  66 + <div v-if="debug" style="margin-top: 8px; color: #999; font-size: 12px;">
  67 + 选项数量: {{ titleOptions.length }}
  68 + <div v-if="titleOptions.length > 0">
  69 + 第一个选项: {{ titleOptions[0].title }} (ID: {{ titleOptions[0].id }})
  70 + </div>
  71 + <div>
  72 + 绑定金额: {{ bindedDeductAmount }}
  73 + 当前输入: {{ input1 }}
  74 + 是否不匹配: {{ amountMismatch }}
  75 + </div>
  76 + </div>
34 77 </div>
35 78 <!-- <template #titleToolbar> <a-button type="primary"> 申请编辑权限 </a-button></template> -->
36 79 <template #appendFooter>
... ... @@ -41,12 +84,12 @@
41 84 </template>
42 85 <script lang="ts" setup>
43 86 import { BasicDrawer, useDrawerInner } from '@/components/Drawer';
44   - import { defineComponent, ref, computed, unref, toRaw, reactive } from 'vue';
45   - import { getEmailList } from '/@/api/sys/config';
46   - import { UploadOutlined } from '@ant-design/icons-vue';
  87 + import { ref, onMounted, computed, watch } from 'vue';
47 88 import type { UploadProps } from 'ant-design-vue';
48 89 import { updateDeduct } from '@/api/project/invoice';
49 90 import { useMessage } from '/@/hooks/web/useMessage';
  91 + import { getAllQuests, queryQuestTitle } from '@/api/project/quest';
  92 + import { handleViewRichText } from '../financeList/finance.data';
50 93  
51 94 const emit = defineEmits(['success']);
52 95 const fileList = ref<UploadProps['fileList']>([]);
... ... @@ -63,16 +106,264 @@
63 106 const { createMessage } = useMessage();
64 107 const { error } = createMessage;
65 108 const status = ref();
  109 +
  110 + // 扣款原因相关状态
  111 + const titleOptions = ref<any[]>([]);
  112 + const selectedTitle = ref<number | undefined>(undefined);
  113 + const selectedTitleText = ref('');
  114 + const titleLoading = ref(false);
  115 + const debug = ref(false); // 关闭调试模式
  116 +
  117 + // 添加绑定的deductAmount变量
  118 + const bindedDeductAmount = ref<number | undefined>(undefined);
  119 + // 添加金额不一致的提示状态
  120 + const amountMismatch = ref(false);
  121 +
  122 + // 转换为antd需要的options格式
  123 + const formattedOptions = computed(() => {
  124 + return titleOptions.value.map(item => ({
  125 + value: item.id,
  126 + label: item.title,
  127 + }));
  128 + });
66 129  
67   - const [register, { setDrawerProps, closeDrawer }] = useDrawerInner((data) => {
  130 + const [register, { closeDrawer }] = useDrawerInner((data) => {
68 131 status.value = data.data.invoiceStatus;
69 132 id.value = data.data.invoiceId;
70 133 invoiceNo.value = data.data.invoiceNo;
71 134 input1.value = data.data.invoiceDeductAmount;
72 135 deductUrl.value = data.data.invoiceDeductUrl;
73 136 deductUrlOld.value = data.data.invoiceDeductUrl;
  137 +
  138 + // 优先使用questId,如果没有则尝试使用invoiceRichTextId
  139 + selectedTitle.value = data.data.questId || data.data.invoiceRichTextId;
  140 +
  141 + // 加载扣款原因选项
  142 + loadQuestOptions();
74 143 });
75 144  
  145 + // 处理API返回的数据,统一格式
  146 + function normalizeData(data: any[]): { id: number; title: string; deductAmount?: number }[] {
  147 +
  148 + if (!data || !Array.isArray(data) || data.length === 0) {
  149 + return [];
  150 + }
  151 +
  152 + const result = data.map(item => {
  153 + // 处理可能的不同字段名称
  154 + const id = item.id !== undefined ? item.id :
  155 + (item.questId !== undefined ? item.questId : null);
  156 +
  157 + const title = item.title !== undefined ? item.title :
  158 + (item.questTitle !== undefined ? item.questTitle :
  159 + (item.name !== undefined ? item.name : '未知标题'));
  160 +
  161 + // 添加新的deductAmount字段处理
  162 + const deductAmount = item.deductAmount !== undefined ? Number(item.deductAmount) : undefined;
  163 +
  164 + return { id, title, deductAmount };
  165 + }).filter(item => item.id !== null); // 过滤掉无效项
  166 + return result;
  167 + }
  168 +
  169 + // 加载所有扣款原因选项
  170 + async function loadQuestOptions() {
  171 + titleLoading.value = true;
  172 + try {
  173 + // 如果已有选中ID但没有选项,先尝试直接获取该ID的详情
  174 + if (selectedTitle.value && (!titleOptions.value || titleOptions.value.length === 0)) {
  175 + try {
  176 + // 使用finance.data.ts中的handleViewRichText方法获取富文本详情
  177 + const richTextDetail = await handleViewRichText(String(selectedTitle.value));
  178 + if (richTextDetail) {
  179 + // 设置选项
  180 + titleOptions.value = [{
  181 + id: richTextDetail.id,
  182 + title: richTextDetail.title || '扣款原因详情',
  183 + deductAmount: richTextDetail.deductAmount // 确保获取deductAmount
  184 + }];
  185 + selectedTitleText.value = richTextDetail.title || '扣款原因详情';
  186 + // 绑定deductAmount如果存在
  187 + if (richTextDetail.deductAmount !== undefined) {
  188 + bindedDeductAmount.value = Number(richTextDetail.deductAmount);
  189 + checkAmountMatch();
  190 + }
  191 + titleLoading.value = false;
  192 + return;
  193 + }
  194 + } catch (err) {
  195 + console.error('获取单个扣款原因详情失败:', err);
  196 + }
  197 + }
  198 +
  199 + const res = await getAllQuests();
  200 +
  201 + // 处理不同格式的返回数据
  202 + let tempData: any[] = [];
  203 +
  204 + if (Array.isArray(res)) {
  205 + tempData = res;
  206 + } else if (res && Array.isArray(res.records)) {
  207 + tempData = res.records;
  208 + } else if (res && Array.isArray(res.items)) {
  209 + tempData = res.items;
  210 + } else if (res && typeof res === 'object') {
  211 + tempData = [res];
  212 + }
  213 +
  214 + // 标准化数据格式
  215 + titleOptions.value = normalizeData(tempData);
  216 +
  217 + // 如果有已选择的ID,查找对应的title用于显示
  218 + if (selectedTitle.value && titleOptions.value.length > 0) {
  219 + const found = titleOptions.value.find(item => item.id === selectedTitle.value);
  220 + if (found) {
  221 + selectedTitleText.value = found.title;
  222 + // 绑定deductAmount如果存在
  223 + if (found.deductAmount !== undefined) {
  224 + bindedDeductAmount.value = Number(found.deductAmount);
  225 + checkAmountMatch();
  226 + }
  227 + } else {
  228 + // 如果在选项中找不到对应ID,可以尝试单独请求这个ID的详情
  229 + try {
  230 + const richTextDetail = await handleViewRichText(String(selectedTitle.value));
  231 + if (richTextDetail) {
  232 + // 添加到选项中
  233 + titleOptions.value.push({
  234 + id: richTextDetail.id,
  235 + title: richTextDetail.title || '扣款原因详情',
  236 + deductAmount: richTextDetail.deductAmount // 确保获取deductAmount
  237 + });
  238 + selectedTitleText.value = richTextDetail.title || '扣款原因详情';
  239 + // 绑定deductAmount如果存在
  240 + if (richTextDetail.deductAmount !== undefined) {
  241 + bindedDeductAmount.value = Number(richTextDetail.deductAmount);
  242 + checkAmountMatch();
  243 + }
  244 + }
  245 + } catch (err) {
  246 + console.error('获取缺失的扣款原因详情失败:', err);
  247 + }
  248 + }
  249 + }
  250 +
  251 + } catch (err) {
  252 + error('获取扣款原因列表失败');
  253 + console.error('获取扣款原因列表失败:', err);
  254 + titleOptions.value = [];
  255 + } finally {
  256 + titleLoading.value = false;
  257 + }
  258 + }
  259 +
  260 + // 搜索扣款原因
  261 + async function handleSearch(value: string) {
  262 + if (!value || value.trim() === '') {
  263 + await loadQuestOptions();
  264 + return;
  265 + }
  266 +
  267 + titleLoading.value = true;
  268 + try {
  269 + const res = await queryQuestTitle(value);
  270 +
  271 + // 确保res不为null或undefined
  272 + if (!res) {
  273 + titleOptions.value = [];
  274 + return;
  275 + }
  276 +
  277 + // 处理不同格式的返回数据
  278 + let tempData: any[] = [];
  279 +
  280 + if (Array.isArray(res)) {
  281 + tempData = res;
  282 + } else if (res && Array.isArray(res.records)) {
  283 + tempData = res.records;
  284 + } else if (res && Array.isArray(res.items)) {
  285 + tempData = res.items;
  286 + } else if (res && typeof res === 'object') {
  287 + tempData = [res];
  288 + }
  289 +
  290 + // 标准化数据格式
  291 + titleOptions.value = normalizeData(tempData);
  292 + } catch (err) {
  293 + error('搜索扣款原因失败');
  294 + console.error('搜索扣款原因失败:', err);
  295 + titleOptions.value = [];
  296 + } finally {
  297 + titleLoading.value = false;
  298 + }
  299 + }
  300 +
  301 + // 选择扣款原因时触发
  302 + function handleTitleChange(value: number, option: any) {
  303 + // 检查是否为空值(用户点击了清除按钮)
  304 + if (value === undefined || value === null) {
  305 + handleClear();
  306 + return;
  307 + }
  308 +
  309 + selectedTitle.value = value;
  310 +
  311 + // 当使用options属性时,option为选中的选项对象
  312 + if (option && typeof option === 'object') {
  313 + if (option.label) {
  314 + // 新版本Ant Design Vue返回的选项格式
  315 + selectedTitleText.value = option.label;
  316 + } else if (option.children) {
  317 + // 旧版本Ant Design Vue返回的选项格式
  318 + selectedTitleText.value = option.children;
  319 + }
  320 + }
  321 +
  322 + // 直接从选项列表中查找
  323 + const found = titleOptions.value.find(item => item.id === value);
  324 + if (found) {
  325 + selectedTitleText.value = found.title;
  326 + // 绑定deductAmount
  327 + bindedDeductAmount.value = found.deductAmount;
  328 + // 检查金额是否一致
  329 + checkAmountMatch();
  330 + }
  331 + }
  332 +
  333 + // 清除选择
  334 + function handleClear() {
  335 + selectedTitle.value = undefined;
  336 + selectedTitleText.value = '';
  337 + // 清除绑定的deductAmount
  338 + bindedDeductAmount.value = undefined;
  339 + // 重置金额不匹配状态
  340 + amountMismatch.value = false;
  341 +
  342 + // 重新加载所有选项
  343 + loadQuestOptions();
  344 + }
  345 +
  346 + // 检查金额是否匹配
  347 + function checkAmountMatch() {
  348 + if (bindedDeductAmount.value !== undefined && input1.value !== undefined) {
  349 + try {
  350 + // 确保转换为数字进行比较,处理可能的字符串转换问题
  351 + const boundAmount = typeof bindedDeductAmount.value === 'string'
  352 + ? parseFloat(bindedDeductAmount.value)
  353 + : Number(bindedDeductAmount.value);
  354 +
  355 + const inputAmount = typeof input1.value === 'string'
  356 + ? parseFloat(input1.value)
  357 + : Number(input1.value);
  358 +
  359 + // 比较金额是否一致,使用差值小于极小值来处理浮点数误差
  360 + amountMismatch.value = Math.abs(boundAmount - inputAmount) > 0.001;
  361 + } catch (err) {
  362 + console.error('金额比较出错:', err);
  363 + }
  364 + }
  365 + }
  366 +
76 367 function handleChange(info) {
77 368 if (info.file.status == 'done') {
78 369 updateDeductUrl.value = info.file.response.data.fileUrl;
... ... @@ -86,33 +377,56 @@
86 377 function beforeUpload(info) {
87 378 updateDeductUrl.value = uploadUrl.value + info.name;
88 379 }
89   - function handleShow() {
90   - // if (!visible) {
91   - // input1.value = 0;
92   - // deductUrl.value = '';
93   - // updateDeductUrl.value = '';
94   - // fileList.value = [];
95   - // }
96   - // input1.value = '';
97   - // deductUrl.value = '';
98   - // updateDeductUrl.value = '';
99   - // fileList.value = [];
  380 + function handleShow(visible) {
  381 + if (visible) {
  382 + // 抽屉打开时加载扣款原因选项
  383 + // loadQuestOptions();
  384 + }
100 385 }
101 386  
  387 + // 页面加载时获取扣款原因列表
  388 + onMounted(() => {
  389 + // loadQuestOptions();
  390 + });
  391 +
  392 + // 监听客户扣款金额变化
  393 + function handleInputChange() {
  394 + // 检查金额是否匹配
  395 + checkAmountMatch();
  396 + }
  397 +
  398 + // 监听input1的变化
  399 + watch(input1, () => {
  400 + handleInputChange();
  401 + }, { immediate: true }); // 确保首次加载也会触发
  402 +
102 403 //完成编辑
103 404 async function handleSubmit() {
104 405 if (!input1.value) {
105   - error('选项不能为空');
106   - } else {
  406 + error('客户扣款金额不能为空');
  407 + return;
  408 + }
  409 +
  410 + // 添加金额不一致的检查
  411 + if (amountMismatch.value) {
  412 + error(`客户扣款金额与选择的扣款原因金额不一致!扣款原因金额为: ${bindedDeductAmount.value}`);
  413 + return;
  414 + }
  415 +
  416 + try {
107 417 await updateDeduct({
108 418 id: id.value,
109 419 invoiceNo: invoiceNo.value,
110 420 deductAmount: input1.value,
111 421 deductUrl: deductUrl.value,
  422 + questId: selectedTitle.value, // 传递选中的扣款原因ID
112 423 });
113 424 fileList.value = [];
114 425 emit('success');
115 426 closeDrawer();
  427 + } catch (err) {
  428 + error('保存失败');
  429 + console.error('保存失败:', err);
116 430 }
117 431 }
118 432 </script>
... ...
src/views/project/finance/financeList/TrackEditCheck.vue
... ... @@ -15,13 +15,22 @@
15 15 >
16 16 <div>
17 17 <div style="font-size: 15px">生产科扣款金额¥</div>
18   - <a-input v-model:value="input1" placeholder="请输入" :disabled="status === 10" auto-size />
  18 + <a-input
  19 + v-model:value="input1"
  20 + placeholder="请输入"
  21 + :disabled="status === 10 || status === 40"
  22 + auto-size
  23 + :status="amountMismatch ? 'error' : ''"
  24 + />
  25 + <div v-if="amountMismatch" style="color: #ff4d4f; font-size: 19px; margin-top: 4px">
  26 + 金额不匹配!扣款原因金额为: {{ bindedDeductAmount }}
  27 + </div>
19 28 <div style="margin: 16px 0"></div>
20 29 <div style="font-size: 15px">扣款责任部门</div>
21 30 <a-input
22 31 v-model:value="deductDept"
23 32 placeholder="请输入"
24   - :disabled="status === 10"
  33 + :disabled="status === 10 || status === 40"
25 34 auto-size
26 35 />
27 36 <!-- <a-select
... ... @@ -44,13 +53,35 @@
44 53 :beforeUpload="beforeUpload"
45 54 list-type="picture"
46 55 :max-count="1"
47   - :disabled="status === 10"
  56 + :disabled="status === 10 || status === 40"
48 57 :action="updateDeductUrl"
49 58 @change="handleChange"
50 59 >
51 60 <a-button> 上传扣款单 </a-button>
52 61 </a-upload>
53 62 </a-space>
  63 +
  64 + <div style="margin: 16px 0"></div>
  65 + <div>扣款原因(可选)</div>
  66 + <a-select
  67 + v-model:value="selectedTitle"
  68 + placeholder="请选择或输入扣款原因"
  69 + style="width: 100%"
  70 + :filter-option="false"
  71 + show-search
  72 + :disabled="status === 10"
  73 + @search="handleSearch"
  74 + @change="handleTitleChange"
  75 + :loading="titleLoading"
  76 + :options="formattedOptions"
  77 + :not-found-content="titleLoading ? '加载中...' : (titleOptions.length === 0 ? '未找到匹配项' : null)"
  78 + allow-clear
  79 + @clear="handleClear"
  80 + :auto-focus="true"
  81 + :default-open="false"
  82 + :drop-down-style="{ maxHeight: '400px', overflow: 'auto' }"
  83 + >
  84 + </a-select>
54 85 </div>
55 86 <!-- <template #titleToolbar> <a-button type="primary"> 申请编辑权限 </a-button></template> -->
56 87 <template #appendFooter>
... ... @@ -61,12 +92,12 @@
61 92 </template>
62 93 <script lang="ts" setup>
63 94 import { BasicDrawer, useDrawerInner } from '@/components/Drawer';
64   - import { defineComponent, ref, computed, unref, toRaw, reactive, onMounted } from 'vue';
65   - import { getEmailList } from '/@/api/sys/config';
66   - import { UploadOutlined } from '@ant-design/icons-vue';
  95 + import { ref, computed, onMounted, watch } from 'vue';
67 96 import type { UploadProps } from 'ant-design-vue';
68 97 import { updateDeductInfo } from '@/api/project/invoice';
69 98 import { useMessage } from '/@/hooks/web/useMessage';
  99 + import { getAllQuests, queryQuestTitle } from '@/api/project/quest';
  100 + import { handleViewRichText } from '../financeList/finance.data';
70 101  
71 102 const emit = defineEmits(['success']);
72 103 const fileList = ref<UploadProps['fileList']>([]);
... ... @@ -83,8 +114,27 @@
83 114 const { createMessage } = useMessage();
84 115 const { error } = createMessage;
85 116 const status = ref();
  117 +
  118 + // 扣款原因相关状态
  119 + const titleOptions = ref<any[]>([]);
  120 + const selectedTitle = ref<number | undefined>(undefined);
  121 + const selectedTitleText = ref('');
  122 + const titleLoading = ref(false);
  123 +
  124 + // 添加绑定的deductAmount变量
  125 + const bindedDeductAmount = ref<number | undefined>(undefined);
  126 + // 添加金额不一致的提示状态
  127 + const amountMismatch = ref(false);
  128 +
  129 + // 转换为antd需要的options格式
  130 + const formattedOptions = computed(() => {
  131 + return titleOptions.value.map(item => ({
  132 + value: item.id,
  133 + label: item.title,
  134 + }));
  135 + });
86 136  
87   - const [register, { setDrawerProps, closeDrawer }] = useDrawerInner((data) => {
  137 + const [register, { closeDrawer }] = useDrawerInner((data) => {
88 138 status.value = data.data.checkPayStatus;
89 139 id.value = data.data.checkId;
90 140 checkNo.value = data.data.checkNo;
... ... @@ -92,7 +142,218 @@
92 142 deductDept.value = data.data.checkDeductDept;
93 143 deductUrl.value = data.data.checkDeductUrl;
94 144 deductUrlOld.value = data.data.checkDeductUrl;
  145 +
  146 + // 优先使用questId,如果没有则尝试使用checkRichTextId
  147 + selectedTitle.value = data.data.questId || data.data.checkRichTextId;
  148 +
  149 + // 加载扣款原因选项
  150 + loadQuestOptions();
95 151 });
  152 +
  153 + // 处理API返回的数据,统一格式
  154 + function normalizeData(data: any[]): { id: number; title: string; deductAmount?: number }[] {
  155 +
  156 + if (!data || !Array.isArray(data) || data.length === 0) {
  157 + return [];
  158 + }
  159 +
  160 + const result = data.map(item => {
  161 + // 处理可能的不同字段名称
  162 + const id = item.id !== undefined ? item.id :
  163 + (item.questId !== undefined ? item.questId : null);
  164 +
  165 + const title = item.title !== undefined ? item.title :
  166 + (item.questTitle !== undefined ? item.questTitle :
  167 + (item.name !== undefined ? item.name : '未知标题'));
  168 +
  169 + // 添加新的deductAmount字段处理
  170 + const deductAmount = item.deductAmount !== undefined ? Number(item.deductAmount) : undefined;
  171 +
  172 + return { id, title, deductAmount };
  173 + }).filter(item => item.id !== null); // 过滤掉无效项
  174 +
  175 + return result;
  176 + }
  177 +
  178 + // 加载所有扣款原因选项
  179 + async function loadQuestOptions() {
  180 + titleLoading.value = true;
  181 + try {
  182 + // 如果已有选中ID但没有选项,先尝试直接获取该ID的详情
  183 + if (selectedTitle.value && (!titleOptions.value || titleOptions.value.length === 0)) {
  184 + try {
  185 + // 使用finance.data.ts中的handleViewRichText方法获取富文本详情
  186 + const richTextDetail = await handleViewRichText(String(selectedTitle.value));
  187 + if (richTextDetail) {
  188 + // 设置选项
  189 + titleOptions.value = [{
  190 + id: richTextDetail.id,
  191 + title: richTextDetail.title || '扣款原因详情',
  192 + deductAmount: richTextDetail.deductAmount // 确保获取deductAmount
  193 + }];
  194 + selectedTitleText.value = richTextDetail.title || '扣款原因详情';
  195 + // 绑定deductAmount如果存在
  196 + if (richTextDetail.deductAmount !== undefined) {
  197 + bindedDeductAmount.value = Number(richTextDetail.deductAmount);
  198 + checkAmountMatch();
  199 + }
  200 + titleLoading.value = false;
  201 + return;
  202 + }
  203 + } catch (err) {
  204 + console.error('获取单个扣款原因详情失败:', err);
  205 + }
  206 + }
  207 +
  208 + const res = await getAllQuests();
  209 +
  210 + // 处理不同格式的返回数据
  211 + let tempData: any[] = [];
  212 +
  213 + if (Array.isArray(res)) {
  214 + tempData = res;
  215 + } else if (res && Array.isArray(res.records)) {
  216 + tempData = res.records;
  217 + } else if (res && Array.isArray(res.items)) {
  218 + tempData = res.items;
  219 + } else if (res && typeof res === 'object') {
  220 + tempData = [res];
  221 + }
  222 +
  223 + // 标准化数据格式
  224 + titleOptions.value = normalizeData(tempData);
  225 +
  226 + // 如果有已选择的ID,查找对应的title用于显示
  227 + if (selectedTitle.value && titleOptions.value.length > 0) {
  228 + const found = titleOptions.value.find(item => item.id === selectedTitle.value);
  229 + if (found) {
  230 + selectedTitleText.value = found.title;
  231 + // 绑定deductAmount如果存在
  232 + if (found.deductAmount !== undefined) {
  233 + bindedDeductAmount.value = Number(found.deductAmount);
  234 + checkAmountMatch();
  235 + }
  236 + } else {
  237 + // 如果在选项中找不到对应ID,可以尝试单独请求这个ID的详情
  238 + try {
  239 + const richTextDetail = await handleViewRichText(String(selectedTitle.value));
  240 + if (richTextDetail) {
  241 + // 添加到选项中
  242 + titleOptions.value.push({
  243 + id: richTextDetail.id,
  244 + title: richTextDetail.title || '扣款原因详情',
  245 + deductAmount: richTextDetail.deductAmount // 确保获取deductAmount
  246 + });
  247 + selectedTitleText.value = richTextDetail.title || '扣款原因详情';
  248 + // 绑定deductAmount如果存在
  249 + if (richTextDetail.deductAmount !== undefined) {
  250 + bindedDeductAmount.value = Number(richTextDetail.deductAmount);
  251 + checkAmountMatch();
  252 + }
  253 + }
  254 + } catch (err) {
  255 + console.error('获取缺失的扣款原因详情失败:', err);
  256 + }
  257 + }
  258 + }
  259 +
  260 + } catch (err) {
  261 + error('获取扣款原因列表失败');
  262 + console.error('获取扣款原因列表失败:', err);
  263 + titleOptions.value = [];
  264 + } finally {
  265 + titleLoading.value = false;
  266 + }
  267 + }
  268 +
  269 + // 搜索扣款原因
  270 + async function handleSearch(value: string) {
  271 + if (!value || value.trim() === '') {
  272 + await loadQuestOptions();
  273 + return;
  274 + }
  275 +
  276 + titleLoading.value = true;
  277 + try {
  278 + const res = await queryQuestTitle(value);
  279 + // 确保res不为null或undefined
  280 + if (!res) {
  281 + titleOptions.value = [];
  282 + return;
  283 + }
  284 +
  285 + // 处理不同格式的返回数据
  286 + let tempData: any[] = [];
  287 +
  288 + if (Array.isArray(res)) {
  289 + tempData = res;
  290 + } else if (res && Array.isArray(res.records)) {
  291 + tempData = res.records;
  292 + } else if (res && Array.isArray(res.items)) {
  293 + tempData = res.items;
  294 + } else if (res && typeof res === 'object') {
  295 + tempData = [res];
  296 + }
  297 +
  298 + // 标准化数据格式
  299 + titleOptions.value = normalizeData(tempData);
  300 +
  301 + // 调试输出处理后的选项
  302 + } catch (err) {
  303 + console.error('搜索扣款原因失败:', err);
  304 + error('搜索扣款原因失败');
  305 + titleOptions.value = [];
  306 + } finally {
  307 + titleLoading.value = false;
  308 + }
  309 + }
  310 +
  311 + // 选择扣款原因时触发
  312 + function handleTitleChange(value: number, option: any) {
  313 +
  314 + // 检查是否为空值(用户点击了清除按钮)
  315 + if (value === undefined || value === null) {
  316 + handleClear();
  317 + return;
  318 + }
  319 +
  320 + selectedTitle.value = value;
  321 +
  322 + // 当使用options属性时,option为选中的选项对象
  323 + if (option && typeof option === 'object') {
  324 + if (option.label) {
  325 + // 新版本Ant Design Vue返回的选项格式
  326 + selectedTitleText.value = option.label;
  327 + } else if (option.children) {
  328 + // 旧版本Ant Design Vue返回的选项格式
  329 + selectedTitleText.value = option.children;
  330 + }
  331 + }
  332 +
  333 + // 直接从选项列表中查找
  334 + const found = titleOptions.value.find(item => item.id === value);
  335 + if (found) {
  336 + selectedTitleText.value = found.title;
  337 + // 绑定deductAmount
  338 + bindedDeductAmount.value = found.deductAmount;
  339 + // 检查金额是否一致
  340 + checkAmountMatch();
  341 + }
  342 + }
  343 +
  344 + // 清除选择
  345 + function handleClear() {
  346 + selectedTitle.value = undefined;
  347 + selectedTitleText.value = '';
  348 + // 清除绑定的deductAmount
  349 + bindedDeductAmount.value = undefined;
  350 + // 重置金额不匹配状态
  351 + amountMismatch.value = false;
  352 +
  353 + // 重新加载所有选项
  354 + loadQuestOptions();
  355 + }
  356 +
96 357 function handleChange(info) {
97 358 if (info.file.status == 'done') {
98 359 updateDeductUrl.value = info.file.response.data.fileUrl;
... ... @@ -106,29 +367,78 @@
106 367 function beforeUpload(info) {
107 368 updateDeductUrl.value = uploadUrl.value + info.name;
108 369 }
109   - function handleShow() {
110   - // input1.value = '';
111   - // deductUrl.value = '';
112   - // deductDept.value = '';
113   - // updateDeductUrl.value = '';
114   - // fileList.value = [];
  370 + function handleShow(visible) {
  371 + if (visible) {
  372 + // 抽屉打开时加载扣款原因选项
  373 + // loadQuestOptions();
  374 + }
115 375 }
  376 +
  377 + // 页面加载时获取扣款原因列表
  378 + onMounted(() => {
  379 + // loadQuestOptions();
  380 + });
  381 +
  382 + // 检查金额是否匹配
  383 + function checkAmountMatch() {
  384 + if (bindedDeductAmount.value !== undefined && input1.value !== undefined) {
  385 + try {
  386 + // 确保转换为数字进行比较,处理可能的字符串转换问题
  387 + const boundAmount = typeof bindedDeductAmount.value === 'string'
  388 + ? parseFloat(bindedDeductAmount.value)
  389 + : Number(bindedDeductAmount.value);
  390 +
  391 + const inputAmount = typeof input1.value === 'string'
  392 + ? parseFloat(input1.value)
  393 + : Number(input1.value);
  394 +
  395 + // 比较金额是否一致,使用差值小于极小值来处理浮点数误差
  396 + amountMismatch.value = Math.abs(boundAmount - inputAmount) > 0.001;
  397 + } catch (err) {
  398 + console.error('金额比较出错:', err);
  399 + }
  400 + }
  401 + }
  402 +
  403 + // 监听客户扣款金额变化
  404 + function handleInputChange() {
  405 + // 检查金额是否匹配
  406 + checkAmountMatch();
  407 + }
  408 +
  409 + // 监听input1的变化
  410 + watch(input1, () => {
  411 + handleInputChange();
  412 + }, { immediate: true }); // 确保首次加载也会触发
  413 +
116 414 //完成编辑
117 415 async function handleSubmit() {
118 416 if (!input1.value || !deductDept.value) {
119 417 error('选项不能为空');
120   - } else {
  418 + return;
  419 + }
  420 +
  421 + // 添加金额不一致的检查
  422 + if (amountMismatch.value) {
  423 + error(`生产科扣款金额与选择的扣款原因金额不一致!扣款原因金额为: ${bindedDeductAmount.value}`);
  424 + return;
  425 + }
  426 +
  427 + try {
121 428 await updateDeductInfo({
122 429 id: id.value,
123 430 checkNo: checkNo.value,
124 431 deductAmount: input1.value,
125 432 deductDept: deductDept.value,
126 433 deductUrl: deductUrl.value,
  434 + questId: selectedTitle.value, // 传递选中的扣款原因ID
127 435 });
128   - // productionDepartment: selectedProductionDepartment.value,
129 436 fileList.value = [];
130 437 emit('success');
131 438 closeDrawer();
  439 + } catch (err) {
  440 + error('保存失败');
  441 + console.error('保存失败:', err);
132 442 }
133 443 }
134 444 </script>
... ...
src/views/project/finance/financeList/finance.data.tsx
1 1 import { FormSchema } from '/@/components/Form';
2 2 import { BasicColumn } from '/@/components/Table';
3   -import { icon } from 'ant-design-vue';
4   -import { FolderAddOutlined, FilePptOutlined } from '@ant-design/icons-vue';
  3 +import { FolderAddOutlined, FilePptOutlined, EyeOutlined } from '@ant-design/icons-vue';
5 4 import { size } from 'lodash-es';
6 5 import { view } from '@/utils/pdfShow';
7 6 import { ref } from 'vue';
8 7 import { queryNoOptions } from '/@/api/project/order';
9 8 import { useOrderStoreWithOut } from '/@/store/modules/order';
10 9 import { useOrderInfo } from '/@/hooks/component/order';
  10 +import { defHttp } from '/@/utils/http/axios';
  11 +import { useMessage } from '/@/hooks/web/useMessage';
11 12  
  13 +const { createMessage } = useMessage();
12 14 const innerNoOptions = ref([]);
13 15 const projectNoOptions = ref([]);
14 16 const orderStore = useOrderStoreWithOut();
... ... @@ -17,6 +19,27 @@ const {
17 19 productionDepartment,
18 20 } = useOrderInfo(orderStore);
19 21  
  22 +// 声明一个全局变量来保存回调函数
  23 +let viewRichTextCallback: ((richTextId: any) => void) | null = null;
  24 +
  25 +// 设置回调函数的方法
  26 +export function setViewRichTextCallback(callback: ((richTextId: any) => void) | null) {
  27 + viewRichTextCallback = callback;
  28 +}
  29 +
  30 +// 直接调用回调函数的方法
  31 +function callViewRichText(richTextId: any) {
  32 + if (viewRichTextCallback) {
  33 + // 显示加载提示
  34 + createMessage.loading({ content: '正在加载扣款原因详情...', duration: 0, key: 'richTextLoading' });
  35 +
  36 + // 调用回调函数
  37 + viewRichTextCallback(richTextId);
  38 + } else {
  39 + console.error('查看回调函数未设置');
  40 + }
  41 +}
  42 +
20 43 export const searchFormSchema: FormSchema[] = [
21 44 {
22 45 field: 'invoiceNo',
... ... @@ -325,6 +348,22 @@ export const columns: BasicColumn[] = [
325 348 },
326 349 },
327 350 {
  351 + title: '扣款原因',
  352 + dataIndex: 'invoiceRichTextId',
  353 + width: 120,
  354 + customRender: ({ record }) => {
  355 + // 如果存在invoiceRichTextId,显示查看按钮
  356 + if (record.invoiceRichTextId) {
  357 + return (
  358 + <a onClick={() => callViewRichText(record.invoiceRichTextId)}>
  359 + <EyeOutlined style="margin-right: 5px" />
  360 + 查看
  361 + </a>
  362 + );
  363 + }
  364 + },
  365 + },
  366 + {
328 367 title: '实际应收金额$',
329 368 dataIndex: 'invoiceActualReceivableAmount',
330 369 width: 120,
... ... @@ -465,6 +504,22 @@ export const columns: BasicColumn[] = [
465 504 },
466 505 },
467 506 {
  507 + title: '扣款原因',
  508 + dataIndex: 'checkRichTextId',
  509 + width: 120,
  510 + customRender: ({ record }) => {
  511 + // 如果存在checkRichTextId,显示查看按钮
  512 + if (record.checkRichTextId) {
  513 + return (
  514 + <a onClick={() => callViewRichText(record.checkRichTextId)}>
  515 + <EyeOutlined style="margin-right: 5px" />
  516 + 查看
  517 + </a>
  518 + );
  519 + }
  520 + },
  521 + },
  522 + {
468 523 title: '生产科实际应付金额¥',
469 524 dataIndex: 'checkActualPayedAmount',
470 525 width: 180,
... ... @@ -555,6 +610,7 @@ export const columns: BasicColumn[] = [
555 610 width: 280,
556 611 },
557 612 ];
  613 +
558 614 function formatDate(input: string): string {
559 615 // 创建一个 Date 对象
560 616 const date = new Date(input);
... ... @@ -566,4 +622,57 @@ function formatDate(input: string): string {
566 622  
567 623 // 返回格式化后的日期字符串
568 624 return `${year}-${month}-${day}`;
  625 +}
  626 +
  627 +// 处理富文本查看API请求
  628 +export async function handleViewRichText(id: string) {
  629 + if (!id) {
  630 + return null;
  631 + }
  632 +
  633 + // createMessage.loading({ content: '正在获取数据...', key: 'richTextLoading', duration: 0 });
  634 +
  635 + try {
  636 + // 使用正确的API路径和POST请求
  637 + const res = await defHttp.post(
  638 + {
  639 + url: '/order/erp/quest/queryRichText_by_id',
  640 + data: { id },
  641 + },
  642 + { errorMessageMode: 'none' }
  643 + );
  644 +
  645 + if (res) {
  646 + // 如果响应中包含data字段,使用该字段内容
  647 + let formattedData = res;
  648 +
  649 + // 确保必要的字段存在
  650 + formattedData.id = formattedData.id || id;
  651 + formattedData.title = formattedData.title || '扣款原因详情';
  652 +
  653 + // 处理内容字段,可能是richText或contentText
  654 + if (!formattedData.contentText && formattedData.richText) {
  655 + formattedData.contentText = formattedData.richText;
  656 + }
  657 + if (formattedData.questType) {
  658 + formattedData.questType = formattedData.questType;
  659 + }
  660 + return formattedData;
  661 + } else {
  662 + return null;
  663 + }
  664 + } catch (error) {
  665 + return null;
  666 + }
  667 +}
  668 +
  669 +// 简化版富文本查看函数(作为后备方案)
  670 +export async function handleViewRichTextSimple(id: string) {
  671 + // 返回一个基本的模拟数据
  672 + return {
  673 + id: id,
  674 + title: '问题原因被删除或者不存在',
  675 + contentText: `<p>问题原因被删除或者不存在</p>`,
  676 + createTime: new Date().toISOString()
  677 + };
569 678 }
570 679 \ No newline at end of file
... ...
src/views/project/finance/financeList/index.vue
... ... @@ -93,6 +93,7 @@
93 93 <CommitCheck @register="registerCommitCheck" @success="handleSuccess" />
94 94 <EditRefundTimeCheck @register="registerEditRefundTimeCheck" @success="handleSuccess" />
95 95 <NoteCheck @register="registerNoteCheck" @success="handleSuccess" />
  96 + <QuestDrawer @register="registerQuestDrawer" @success="handleSuccess" />
96 97 </template>
97 98 <template #bodyCell="{ column, record }">
98 99 <template v-if="column.key === 'picUrl'">
... ... @@ -288,10 +289,15 @@
288 289 </div>
289 290 </template>
290 291 <script lang="ts" setup>
291   - import { computed, defineComponent, onMounted, ref, watchEffect } from 'vue';
  292 + import { computed, defineComponent, onMounted, ref, watchEffect, onUnmounted } from 'vue';
292 293 import { BasicTable, useTable, BasicColumn, TableAction } from '/@/components/Table';
293   - // import { searchFormSchema, columns } from './receive.data';
294   - import { searchFormSchema, columns } from './finance.data';
  294 + import {
  295 + searchFormSchema,
  296 + columns,
  297 + setViewRichTextCallback,
  298 + handleViewRichText,
  299 + handleViewRichTextSimple,
  300 + } from './finance.data';
295 301 import FinanceEdit from './FinanceEdit.vue';
296 302 import TrackEdit from './TrackEdit.vue';
297 303 import InvoiceAnalysis from './InvoiceAnalysis.vue';
... ... @@ -333,11 +339,13 @@
333 339 import { useUserStoreWithOut } from '/@/store/modules/user';
334 340 import { useMessage } from '/@/hooks/web/useMessage';
335 341 import { useOrderStoreWithOut } from '/@/store/modules/order';
  342 + import QuestDrawer from '/@/views/project/quest/QuestDrawer.vue';
336 343  
337 344 const [registerInvoiceAnalysis, { openModal: openInvoiceAnalysis }] = useModal();
338 345 const [registerFinanceEdit, { openDrawer: openFinanceEdit }] = useDrawer();
339 346 const [registerTrackEdit, { openDrawer: openTrackEdit }] = useDrawer();
340 347 const [registerInvoiceDetail, { openDrawer: openInvoiceDetail }] = useDrawer();
  348 + const [registerQuestDrawer, { openDrawer: openQuestDrawer }] = useDrawer();
341 349 const [registerCommit, { openModal: openCommit }] = useModal();
342 350 const [registerEditRefundTime, { openModal: openEditRefundTime }] = useModal();
343 351 const [registerReUploadBgUrl, { openModal: openReUploadBgUrl }] = useModal();
... ... @@ -394,8 +402,59 @@
394 402 // },
395 403 });
396 404  
  405 + // 设置扣款原因查看回调函数 - 放在全局位置以确保一直有效
  406 + setViewRichTextCallback(async (richTextId) => {
  407 + try {
  408 + // 获取富文本数据
  409 + const data = await handleViewRichText(richTextId);
  410 +
  411 + // 关闭加载提示
  412 + createMessage.destroy('richTextLoading');
  413 +
  414 + if (data) {
  415 + // 创建一个适配QuestDrawer的记录对象
  416 + const questRecord = {
  417 + id: data.id,
  418 + title: data.title || '扣款原因详情',
  419 + contentText: data.contentText || '',
  420 + contentImages: data.contentImages || [],
  421 + files: data.files || [],
  422 + questType: data.questType || ''
  423 + };
  424 +
  425 + // 打开抽屉组件显示数据
  426 + openQuestDrawer(true, {
  427 + record: questRecord,
  428 + isUpdate: true,
  429 + isView: true,
  430 + });
  431 + } else {
  432 + // API请求失败,使用简化版函数创建模拟数据
  433 + const fallbackData = await handleViewRichTextSimple(richTextId);
  434 +
  435 + openQuestDrawer(true, {
  436 + record: fallbackData,
  437 + isUpdate: true,
  438 + isView: true,
  439 + });
  440 + }
  441 + } catch (error) {
  442 + // 关闭加载提示
  443 + createMessage.destroy('richTextLoading');
  444 + createMessage.error('查看扣款原因时发生错误,请联系管理员');
  445 + }
  446 + });
  447 +
397 448 onMounted(async () => {
398 449 await orderStore.getDict();
  450 +
  451 + // 回调函数已移到全局位置
  452 + });
  453 +
  454 + // 组件卸载时清除回调函数
  455 + onUnmounted(() => {
  456 + // 清除回调函数
  457 + setViewRichTextCallback(null);
399 458 });
400 459  
401 460 // 单选函数
... ... @@ -516,7 +575,6 @@
516 575 });
517 576 }
518 577 function handleDeleteDeduct(record) {
519   - console.log(record, '5656record');
520 578 deleteDeduct({ id: record.invoiceId });
521 579 }
522 580 function handleCheckDeleteDeduct(record) {
... ...
src/views/project/quest/QuestDrawer.vue
... ... @@ -6,12 +6,12 @@
6 6 :title="getTitle"
7 7 width="70%"
8 8 @ok="handleSubmit"
9   - :okButtonProps="{ disabled: isView }"
  9 + :okButtonProps="{ disabled: unref(isView) }"
10 10 >
11 11 <BasicForm @register="registerForm" />
12 12  
13 13 <!-- 自定义图片上传区域 -->
14   - <div class="custom-image-upload" v-if="!isView">
  14 + <div class="custom-image-upload" v-if="!unref(isView)">
15 15 <div class="upload-title">图片上传</div>
16 16 <div class="upload-content">
17 17 <div
... ... @@ -50,7 +50,7 @@
50 50 </div>
51 51  
52 52 <!-- 查看模式下的图片显示 -->
53   - <div class="custom-image-view" v-if="isView && uploadedImages.length > 0">
  53 + <div class="custom-image-view" v-if="unref(isView) && uploadedImages.length > 0">
54 54 <div class="upload-title">图片附件</div>
55 55 <div class="upload-content">
56 56 <div
... ... @@ -73,7 +73,7 @@
73 73 </div>
74 74  
75 75 <!-- 自定义附件上传区域 -->
76   - <div class="custom-file-upload" v-if="!isView">
  76 + <div class="custom-file-upload" v-if="!unref(isView)">
77 77 <div class="upload-title">附件上传</div>
78 78 <div class="file-list">
79 79 <!-- 附件列表 -->
... ... @@ -108,7 +108,7 @@
108 108 </div>
109 109  
110 110 <!-- 查看模式下的附件显示 -->
111   - <div class="custom-file-view" v-if="isView && uploadedFiles.length > 0">
  111 + <div class="custom-file-view" v-if="unref(isView) && uploadedFiles.length > 0">
112 112 <div class="upload-title">文件附件</div>
113 113 <div class="file-list">
114 114 <!-- 附件列表 -->
... ... @@ -126,7 +126,7 @@
126 126 </BasicDrawer>
127 127 </template>
128 128  
129   -<script lang="ts" setup>
  129 +<script lang="ts" setup name="QuestDrawer">
130 130 import { ref, computed, unref, onMounted } from 'vue';
131 131 import { BasicForm, useForm } from '/@/components/Form';
132 132 import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
... ... @@ -135,6 +135,7 @@
135 135 import { h } from 'vue';
136 136 import { questCreate, questUpdate } from '/@/api/project/quest';
137 137 import { PlusOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons-vue';
  138 + import { defHttp } from '/@/utils/http/axios';
138 139  
139 140 const emit = defineEmits(['success', 'register']);
140 141 const isUpdate = ref(true);
... ... @@ -173,6 +174,61 @@
173 174 }>>([]);
174 175 const isFileUploading = ref(false);
175 176  
  177 + // 问题类型相关
  178 + const questTypeList = ref<any[]>([]);
  179 + const loadingQuestTypes = ref<boolean>(false);
  180 +
  181 + // 货币类型选择
  182 + const currencyType = ref<string>('0'); // 默认美元
  183 + const currencySymbol = computed(() => currencyType.value === '0' ? '$' : '¥');
  184 + const currencyOptions = [
  185 + { label: '美元 ($)', value: '0' },
  186 + { label: '人民币 (¥)', value: '1' }
  187 + ];
  188 +
  189 + // 问题类型选项计算属性
  190 + const questTypeOptions = computed(() => {
  191 + return questTypeList.value.map(item => ({
  192 + label: item.settingValue,
  193 + value: item.settingValue,
  194 + }));
  195 + });
  196 +
  197 + // 获取问题类型列表
  198 + async function fetchQuestTypes() {
  199 + loadingQuestTypes.value = true;
  200 + try {
  201 + const result = await defHttp.post({
  202 + url: '/order/erp/system_setting/query_list',
  203 + data: { settingCode: 'questType' },
  204 + });
  205 +
  206 + // 简化判断逻辑,直接检查必要条件
  207 + if (result && typeof result === 'object') {
  208 + if (result.result === 0 && Array.isArray(result.data)) {
  209 + // 标准返回格式:{ data: [], result: 0, message: "" }
  210 + questTypeList.value = result.data;
  211 + } else if (Array.isArray(result)) {
  212 + // 另一种可能:直接返回数组
  213 + questTypeList.value = result;
  214 + } else {
  215 + // 数据格式不符合预期
  216 + questTypeList.value = [];
  217 + createMessage.error('获取问题类型列表失败: 数据格式不符合预期');
  218 + }
  219 + } else {
  220 + questTypeList.value = [];
  221 + createMessage.error('获取问题类型列表失败: 无响应数据');
  222 + }
  223 + } catch (error) {
  224 + console.error('QuestDrawer-获取问题类型列表出错:', error);
  225 + questTypeList.value = [];
  226 + createMessage.error('获取问题类型列表失败: ' + (error.message || '未知错误'));
  227 + } finally {
  228 + loadingQuestTypes.value = false;
  229 + }
  230 + }
  231 +
176 232 // 确保URL是完整路径
177 233 function ensureFullUrl(url) {
178 234 if (!url) return '';
... ... @@ -193,7 +249,7 @@
193 249 }
194 250 // 组件加载完成时执行
195 251 onMounted(() => {
196   - console.log('QuestDrawer组件已加载,自定义文件上传和预览功能已配置');
  252 + fetchQuestTypes();
197 253 });
198 254  
199 255 // 点击上传按钮
... ... @@ -234,24 +290,17 @@
234 290 }
235 291  
236 292 const data = await response.json();
237   - console.log('附件上传返回数据:', data);
238 293  
239 294 // 查找fileUrl字段
240 295 let fileUrl = null;
241 296 if (data.data && data.data.fileUrl) {
242 297 fileUrl = data.data.fileUrl;
243   - console.log("在data.data中找到fileUrl:", fileUrl);
244 298 } else {
245   - console.error("未找到fileUrl字段! 数据:", data);
246   - }
247   -
248   - if (!fileUrl) {
249 299 throw new Error('无法获取文件URL');
250 300 }
251 301  
252 302 // 确保URL是完整路径
253 303 const fullUrl = ensureFullUrl(fileUrl);
254   - console.log('完整文件URL:', fullUrl);
255 304  
256 305 // 从URL中提取文件名
257 306 const fileName = extractFileNameFromUrl(fullUrl);
... ... @@ -265,7 +314,6 @@
265 314 createMessage.success(`${fileName} 上传成功`);
266 315 return true;
267 316 } catch (error) {
268   - console.error(`上传文件 ${file.name} 失败:`, error);
269 317 createMessage.error(`${file.name} 上传失败: ${error.message || '未知错误'}`);
270 318 return false;
271 319 }
... ... @@ -289,32 +337,27 @@
289 337 // 尝试从URL中提取文件名
290 338 // 移除查询参数
291 339 const urlWithoutParams = url.split('?')[0];
292   - console.log('移除查询参数后的URL:', urlWithoutParams);
293 340  
294 341 // 获取最后一个路径部分
295 342 let fileName = urlWithoutParams.split('/').pop();
296   - console.log('提取的文件名:', fileName);
297 343  
298 344 // 如果有编码的部分,尝试解码
299 345 if (fileName && (fileName.includes('%') || fileName.includes('+'))) {
300 346 try {
301 347 fileName = decodeURIComponent(fileName);
302   - console.log('解码后的文件名:', fileName);
303 348 } catch (e) {
304   - console.error('解码文件名失败:', e);
  349 + // 解码失败
305 350 }
306 351 }
307 352  
308 353 return fileName || '未知文件';
309 354 } catch (error) {
310   - console.error('提取文件名失败:', error);
311 355 return '未知文件';
312 356 }
313 357 }
314 358  
315 359 // 预览文件
316 360 function previewFile(file) {
317   - console.log('预览文件:', file);
318 361 if (!file.fileUrl) {
319 362 createMessage.error('无法预览:找不到文件URL');
320 363 return;
... ... @@ -329,7 +372,6 @@
329 372  
330 373 // 下载文件
331 374 function downloadFile(file) {
332   - console.log('下载文件:', file);
333 375 if (!file.fileUrl) {
334 376 createMessage.error('无法下载:找不到文件URL');
335 377 return;
... ... @@ -364,10 +406,8 @@
364 406 try {
365 407 const formData = new FormData();
366 408 formData.append('file', file);
367   - formData.append('name', file.name); // 显式添加文件名
368   - console.log('formData',formData);
369   - console.log('file',file);
370   - console.log('name',file.name);
  409 + formData.append('name', file.name);
  410 +
371 411 const response = await fetch(imageUploadUrl.value, {
372 412 method: 'POST',
373 413 body: formData,
... ... @@ -378,7 +418,6 @@
378 418 }
379 419  
380 420 const data = await response.json();
381   - console.log('图片上传返回数据', data);
382 421  
383 422 // 根据接口返回格式获取URL
384 423 let imageUrl = '';
... ... @@ -391,7 +430,6 @@
391 430 imageUrl = data.data.smallPicUrl;
392 431 }
393 432 }
394   -
395 433  
396 434 // 确保URL是完整路径
397 435 if (imageUrl && !imageUrl.startsWith('http')) {
... ... @@ -409,8 +447,6 @@
409 447 }
410 448 }
411 449  
412   - console.log('处理后的图片URL:', imageUrl);
413   -
414 450 // 只添加URL,不添加name
415 451 uploadedImages.value.push({
416 452 url: imageUrl
... ... @@ -418,10 +454,10 @@
418 454  
419 455 createMessage.success('图片上传成功');
420 456 } catch (error) {
421   - console.error('图片上传失败:', error);
422   - createMessage.error(`上传失败: ${error.message || '未知错误'}`);
  457 + createMessage.error(`图片上传失败: ${error.message || '未知错误'}`);
423 458 } finally {
424 459 isUploading.value = false;
  460 +
425 461 // 清空input,以便于再次选择同一文件
426 462 if (imageFileInput.value) {
427 463 imageFileInput.value.value = '';
... ... @@ -437,7 +473,6 @@
437 473 // 处理图片加载错误
438 474 function handleImageError(event: Event, index: number) {
439 475 const target = event.target as HTMLImageElement;
440   - console.error('图片加载失败:', uploadedImages.value[index].url);
441 476  
442 477 // 尝试使用不同格式的URL
443 478 const currentUrl = uploadedImages.value[index].url;
... ... @@ -449,19 +484,22 @@
449 484 newUrl = currentUrl.replace('picUrl', 'smallPicUrl');
450 485 }
451 486  
452   - console.log('尝试新的URL:', newUrl);
453   -
454   - // 使用新URL
455   - uploadedImages.value[index].url = newUrl;
456   - target.src = newUrl;
457   -
458   - // 添加再次失败的处理
459   - target.onerror = () => {
460   - console.error('备选URL也加载失败:', newUrl);
  487 + // 如果有新URL,尝试使用它
  488 + if (newUrl) {
  489 + uploadedImages.value[index].url = newUrl;
  490 + target.src = newUrl;
  491 +
  492 + // 添加再次失败的处理
  493 + target.onerror = () => {
  494 + createMessage.error('图片无法加载,请重新上传');
  495 + // 移除无法加载的图片
  496 + removeImage(index);
  497 + };
  498 + } else {
461 499 createMessage.error('图片无法加载,请重新上传');
462 500 // 移除无法加载的图片
463 501 removeImage(index);
464   - };
  502 + }
465 503 }
466 504  
467 505 // 表单配置
... ... @@ -483,6 +521,49 @@
483 521 },
484 522 },
485 523 {
  524 + field: 'questType',
  525 + label: '问题类型',
  526 + component: 'Select' as any,
  527 + required: true,
  528 + componentProps: () => ({
  529 + options: questTypeOptions.value,
  530 + disabled: unref(isView), // 查看模式下禁用
  531 + loading: loadingQuestTypes.value,
  532 + }),
  533 + },
  534 + {
  535 + field: 'currencySelector',
  536 + label: '扣款金额',
  537 + component: 'Select' as any,
  538 + defaultValue: '0',
  539 + required: true,
  540 + componentProps: {
  541 + options: currencyOptions,
  542 + disabled: unref(isView),
  543 + style: 'width: 100%',
  544 + placeholder: '选择货币',
  545 + onChange: (val: string) => {
  546 + currencyType.value = val;
  547 + }
  548 + },
  549 + colProps: { span: 6 },
  550 + },
  551 + {
  552 + field: 'deductAmount',
  553 + label: ' ',
  554 + component: 'InputNumber' as any,
  555 + required: true,
  556 + componentProps: {
  557 + disabled: unref(isView),
  558 + min: 0,
  559 + precision: 2,
  560 + style: 'width: 100%',
  561 + addonBefore: currencySymbol.value,
  562 + placeholder: '请输入扣款金额',
  563 + },
  564 + colProps: { span: 18 },
  565 + },
  566 + {
486 567 field: 'contentText',
487 568 label: '内容',
488 569 component: 'Input' as any,
... ... @@ -516,37 +597,54 @@
516 597 setDrawerProps({ confirmLoading: false });
517 598 uploadedImages.value = []; // 清空已上传图片
518 599 uploadedFiles.value = []; // 清空已上传文件
  600 +
  601 + // 每次打开表单时都刷新问题类型列表数据
  602 + await fetchQuestTypes();
519 603  
  604 + // 明确设置状态,避免混淆
520 605 isUpdate.value = !!data?.isUpdate;
521 606 isView.value = !!data?.isView; // 设置查看模式
  607 +
  608 + // 确保确认按钮状态正确
  609 + setDrawerProps({
  610 + okButtonProps: {
  611 + disabled: unref(isView)
  612 + }
  613 + });
522 614  
523 615 if (unref(isUpdate)) {
524 616 record.value = { ...data.record };
525   - console.log('加载记录数据:', data.record);
526   - console.log('记录ID:', data.record?.id);
  617 +
  618 + // 设置货币类型
  619 + if (data.record.isRmb !== undefined) {
  620 + currencyType.value = data.record.isRmb;
  621 + }
527 622  
528 623 // 确保ID字段被正确保存,即使它不在表单字段中
529 624 if (data.record && data.record.id) {
530 625 record.value.id = data.record.id;
531   - console.log('保存记录ID到record:', record.value.id);
532 626 }
533 627  
534   - await setFieldsValue({
535   - ...data.record,
536   - });
  628 + // 延迟一帧后设置表单值,确保DOM已更新
  629 + setTimeout(async () => {
  630 + await setFieldsValue({
  631 + ...data.record,
  632 + // 确保扣款金额正确加载
  633 + deductAmount: data.record.deductAmount !== undefined ? Number(data.record.deductAmount) : undefined,
  634 + // 设置货币选择器的值
  635 + currencySelector: data.record.isRmb !== undefined ? data.record.isRmb : '0',
  636 + });
  637 + }, 0);
537 638  
538   - // 处理图片数据 - 现在contentImages是一个URL数组
  639 + // 处理图片数据
539 640 try {
540 641 if (data.record.contentImages) {
541   - console.log('原始图片数据:', data.record.contentImages);
542   -
543 642 // 处理contentImages数组
544 643 if (Array.isArray(data.record.contentImages)) {
545 644 // 直接使用数组
546 645 uploadedImages.value = data.record.contentImages.map(url => ({
547 646 url: ensureFullUrl(url)
548 647 }));
549   - console.log('数组格式的图片URL:', uploadedImages.value);
550 648 }
551 649 // 向下兼容旧格式,如果是字符串,尝试解析
552 650 else if (typeof data.record.contentImages === 'string') {
... ... @@ -573,14 +671,10 @@
573 671 }));
574 672 }
575 673 }
576   -
577   - console.log('处理后的图片数据:', uploadedImages.value);
578 674 }
579 675  
580 676 // 处理附件数据 - 现在files是一个URL数组
581 677 if (data.record.files) {
582   - console.log('原始文件数据:', data.record.files);
583   -
584 678 // 处理files数组
585 679 if (Array.isArray(data.record.files)) {
586 680 // 直接使用数组
... ... @@ -591,7 +685,6 @@
591 685 fileUrl: fileUrl
592 686 };
593 687 });
594   - console.log('数组格式的文件URL:', uploadedFiles.value);
595 688 }
596 689 // 向下兼容旧格式,如果是字符串,尝试解析
597 690 else if (typeof data.record.files === 'string') {
... ... @@ -645,11 +738,9 @@
645 738 });
646 739 }
647 740 }
648   -
649   - console.log('处理后的文件数据:', uploadedFiles.value);
650 741 }
651 742 } catch (error) {
652   - console.error('处理附件和图片数据时出错:', error);
  743 + // 处理附件和图片数据时出错
653 744 }
654 745 }
655 746 });
... ... @@ -664,26 +755,32 @@
664 755  
665 756 // 提交表单
666 757 async function handleSubmit() {
667   - // 查看模式下不允许提交
668   - if (unref(isView)) return;
  758 + // 查看模式下直接返回,不执行任何操作
  759 + if (unref(isView)) {
  760 + closeDrawer();
  761 + return;
  762 + }
669 763  
670 764 try {
671 765 const values = await validate();
672   - console.log('values的值',values);
673 766 setDrawerProps({ confirmLoading: true });
674 767  
  768 + // 添加货币类型参数
  769 + values.isRmb = currencyType.value;
  770 +
  771 + // 移除辅助字段,不需要提交到后端
  772 + delete values.currencySelector;
  773 +
675 774 // 添加上传的图片到表单数据中 - 直接提交URL数组
676 775 if (uploadedImages.value.length > 0) {
677 776 // 直接将URL数组作为contentImages
678 777 values.contentImages = uploadedImages.value.map(img => img.url);
679   - console.log('图片数据(数组格式):', values.contentImages);
680 778 }
681 779  
682 780 // 添加上传的附件到表单数据中 - 直接提交URL数组
683 781 if (uploadedFiles.value.length > 0) {
684 782 // 直接将fileUrl数组作为files
685 783 values.files = uploadedFiles.value.map(file => file.fileUrl);
686   - console.log('附件数据(数组格式):', values.files);
687 784 }
688 785  
689 786 // 如果是更新模式,确保添加ID
... ... @@ -692,44 +789,32 @@
692 789 // 从record中获取ID
693 790 if (record.value && record.value.id) {
694 791 values.id = record.value.id;
695   - console.log('从record中添加记录ID:', values.id);
696 792 } else {
697   - console.warn('警告:更新模式下未找到记录ID!');
  793 + createMessage.error('更新失败:缺少记录ID');
  794 + setDrawerProps({ confirmLoading: false });
  795 + return;
698 796 }
699 797 }
700 798  
701   - // 确保表单提交前检查ID
702   - if (isUpdateValue && !values.id) {
703   - console.error('错误:更新模式下缺少ID字段!');
704   - createMessage.error('更新失败:缺少记录ID');
705   - setDrawerProps({ confirmLoading: false });
706   - return;
707   - }
708   -
709   - // 提交数据
710   - console.log('表单数据', values);
711   -
712 799 // 调用后端API保存数据
713 800 const requestApi = isUpdateValue
714 801 ? questUpdate // 更新接口
715   - : questCreate; // 创建接口
716   - console.log('requestApi调用',requestApi);
717   - const res=await requestApi(values);
718   - console.log('res返回数据',res);
719   - // 关闭抽屉
  802 + : questCreate; // 创建接口
  803 +
  804 + await requestApi(values);
  805 +
  806 + // 关闭抽屉
720 807 closeDrawer();
721 808 // 通知父组件刷新数据
722 809 emit('success');
723 810 setDrawerProps({ confirmLoading: false });
724 811 } catch (error) {
725   - console.error('API请求失败:', error);
726 812 createMessage.error(`保存失败: ${error.message || '未知错误'}`);
727 813 }
728 814 }
729 815  
730 816 // 预览图片
731 817 function previewImage(image) {
732   - console.log('预览图片:', image);
733 818 if (!image.url) {
734 819 createMessage.error('无法预览:找不到图片URL');
735 820 return;
... ... @@ -780,6 +865,12 @@
780 865 }
781 866 </script>
782 867  
  868 +<script lang="ts">
  869 +export default {
  870 + name: 'QuestDrawer',
  871 +};
  872 +</script>
  873 +
783 874 <style lang="less" scoped>
784 875 .custom-image-upload {
785 876 margin-top: 20px;
... ...
src/views/project/quest/QuestTypeModal.vue 0 → 100644
  1 +<template>
  2 + <a-modal
  3 + :visible="visible"
  4 + title="问题类型管理"
  5 + @cancel="handleCancel"
  6 + :footer="null"
  7 + width="800px"
  8 + >
  9 + <div class="quest-type-container">
  10 + <!-- 列表展示区域 -->
  11 + <div class="list-header">
  12 + <div class="title">问题类型</div>
  13 + <a-button type="primary" @click="handleAddNew" size="large">
  14 + 新增
  15 + </a-button>
  16 + </div>
  17 +
  18 + <!-- 问题类型列表 -->
  19 + <a-table
  20 + :dataSource="typeList"
  21 + :columns="columns"
  22 + :pagination="false"
  23 + :loading="loading"
  24 + rowKey="id"
  25 + size="large"
  26 + >
  27 + <template #bodyCell="{ column, record }">
  28 + <template v-if="column.key === 'action'">
  29 + <a-popconfirm
  30 + v-if="role == ROLE.ADMIN"
  31 + title="确定要删除此问题类型吗?"
  32 + @confirm="handleDelete(record)"
  33 + okText="确定"
  34 + cancelText="取消"
  35 + >
  36 + <a-button type="link" danger size="large" >删除</a-button>
  37 + </a-popconfirm>
  38 + </template>
  39 + </template>
  40 + </a-table>
  41 + </div>
  42 +
  43 + <!-- 新增问题类型弹框 -->
  44 + <a-modal
  45 + v-model:visible="addModalVisible"
  46 + title="新增问题类型"
  47 + @ok="handleAddSubmit"
  48 + @cancel="addModalVisible = false"
  49 + :confirmLoading="addLoading"
  50 + width="500px"
  51 + >
  52 + <a-form :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
  53 + <a-form-item label="问题类型名称" :rules="[{ required: true, message: '请输入问题类型名称' }]">
  54 + <a-input v-model:value="newTypeName" placeholder="请输入问题类型名称" size="large" />
  55 + </a-form-item>
  56 + </a-form>
  57 + </a-modal>
  58 + </a-modal>
  59 +</template>
  60 +
  61 +<script lang="ts" setup name="QuestTypeModal">
  62 + import { ref, onMounted, watch, defineProps, defineEmits, computed } from 'vue';
  63 + import { useMessage } from '/@/hooks/web/useMessage';
  64 + import { defHttp } from '/@/utils/http/axios';
  65 + import { ROLE } from '../order/type.d';
  66 + import {useUserStoreWithOut} from '@/store/modules/user'
  67 + const userStore = useUserStoreWithOut();
  68 + const user=userStore.getUserInfo;
  69 +
  70 + const props = defineProps({
  71 + visible: {
  72 + type: Boolean,
  73 + default: false,
  74 + },
  75 + });
  76 +
  77 + const emit = defineEmits(['update:visible', 'success']);
  78 +
  79 + const { createMessage } = useMessage();
  80 +
  81 + // 问题类型列表相关
  82 + const typeList = ref<any[]>([]);
  83 + const loading = ref<boolean>(false);
  84 + const columns = [
  85 + {
  86 + title: '问题类型',
  87 + dataIndex: 'settingValue',
  88 + key: 'settingValue',
  89 + width: '70%',
  90 + },
  91 + {
  92 + title: '操作',
  93 + key: 'action',
  94 + width: '30%',
  95 + align: 'center',
  96 + },
  97 + ];
  98 +
  99 + // 新增问题类型相关
  100 + const addModalVisible = ref<boolean>(false);
  101 + const addLoading = ref<boolean>(false);
  102 + const newTypeName = ref<string>('');
  103 + const role = computed(() =>{
  104 + return user?.roleSmallVO?.code;
  105 + })
  106 + // 监听visible变化,当打开时获取数据
  107 + watch(
  108 + () => props.visible,
  109 + (val) => {
  110 + if (val) {
  111 + fetchTypeList();
  112 + }
  113 + }
  114 + );
  115 +
  116 + // 获取问题类型列表
  117 + async function fetchTypeList() {
  118 + loading.value = true;
  119 + try {
  120 + const result = await defHttp.post({
  121 + url: '/order/erp/system_setting/query_list',
  122 + data: { settingCode: 'questType' },
  123 + });
  124 +
  125 + // 简化判断逻辑,直接检查必要条件
  126 + if (result && typeof result === 'object') {
  127 + if (result.result === 0 && Array.isArray(result.data)) {
  128 + // 标准返回格式:{ data: [], result: 0, message: "" }
  129 + typeList.value = result.data;
  130 + } else if (Array.isArray(result)) {
  131 + // 另一种可能:直接返回数组
  132 + typeList.value = result;
  133 + } else {
  134 + // 数据格式不符合预期
  135 + typeList.value = [];
  136 + console.error('数据格式不符合预期:', result);
  137 + createMessage.error('获取问题类型列表失败: 数据格式不符合预期');
  138 + }
  139 + } else {
  140 + typeList.value = [];
  141 + createMessage.error('获取问题类型列表失败: 无响应数据');
  142 + }
  143 + } catch (error) {
  144 + console.error('获取问题类型列表出错:', error);
  145 + typeList.value = [];
  146 + createMessage.error('获取问题类型列表失败: ' + (error.message || '未知错误'));
  147 + } finally {
  148 + loading.value = false;
  149 + }
  150 + }
  151 +
  152 + // 关闭弹框
  153 + function handleCancel() {
  154 + emit('update:visible', false);
  155 + }
  156 +
  157 + // 打开新增弹框
  158 + function handleAddNew() {
  159 + newTypeName.value = '';
  160 + addModalVisible.value = true;
  161 + }
  162 +
  163 + // 提交新增
  164 + async function handleAddSubmit() {
  165 + if (!newTypeName.value.trim()) {
  166 + createMessage.warning('请输入问题类型名称');
  167 + return;
  168 + }
  169 +
  170 + addLoading.value = true;
  171 + try {
  172 + const result = await defHttp.post({
  173 + url: '/order/erp/system_setting/add',
  174 + data: {
  175 + settingCode: 'questType',
  176 + settingName: '问题类型',
  177 + settingValue: newTypeName.value.trim(),
  178 + settingType: 0,
  179 + relationCode: 'questType',
  180 + relationName: '问题类型',
  181 + relationValue: newTypeName.value.trim(),
  182 + },
  183 + });
  184 +
  185 +
  186 +
  187 + // 放宽判断条件
  188 + if (result) {
  189 + // 不再强制检查result.result === 0
  190 + createMessage.success('新增问题类型成功');
  191 + addModalVisible.value = false;
  192 + await fetchTypeList(); // 刷新列表
  193 + emit('success'); // 通知父组件操作成功
  194 + } else {
  195 + console.error('添加问题类型失败:', result);
  196 + createMessage.error('新增问题类型失败');
  197 + }
  198 + } catch (error) {
  199 + console.error('添加问题类型出错:', error);
  200 + createMessage.error('新增问题类型失败: ' + (error.message || '未知错误'));
  201 + } finally {
  202 + addLoading.value = false;
  203 + }
  204 + }
  205 +
  206 + // 删除问题类型
  207 + async function handleDelete(record) {
  208 + loading.value = true;
  209 + try {
  210 + const result = await defHttp.post({
  211 + url: '/order/erp/system_setting/delete_by_id',
  212 + data: { ids: [record.id] },
  213 + });
  214 +
  215 +
  216 +
  217 + // 放宽判断条件
  218 + if (result) {
  219 + // 不再强制检查result.result === 0
  220 + createMessage.success('删除问题类型成功');
  221 + await fetchTypeList(); // 刷新列表
  222 + emit('success'); // 通知父组件操作成功
  223 + } else {
  224 + console.error('删除问题类型失败:', result);
  225 + createMessage.error('删除问题类型失败');
  226 + }
  227 + } catch (error) {
  228 + console.error('删除问题类型出错:', error);
  229 + createMessage.error('删除问题类型失败: ' + (error.message || '未知错误'));
  230 + } finally {
  231 + loading.value = false;
  232 + }
  233 + }
  234 +
  235 + // 组件挂载时获取数据
  236 + onMounted(() => {
  237 + if (props.visible) {
  238 + fetchTypeList();
  239 + }
  240 + });
  241 +</script>
  242 +
  243 +<script lang="ts">
  244 +export default {
  245 + name: 'QuestTypeModal',
  246 +};
  247 +</script>
  248 +
  249 +<style lang="less" scoped>
  250 + .quest-type-container {
  251 + .list-header {
  252 + display: flex;
  253 + justify-content: space-between;
  254 + align-items: center;
  255 + margin-bottom: 24px;
  256 +
  257 + .title {
  258 + font-size: 18px;
  259 + font-weight: 500;
  260 + }
  261 + }
  262 +
  263 + :deep(.ant-table) {
  264 + font-size: 16px;
  265 +
  266 + .ant-table-thead > tr > th {
  267 + font-size: 16px;
  268 + padding: 16px 16px;
  269 + }
  270 +
  271 + .ant-table-tbody > tr > td {
  272 + padding: 16px 16px;
  273 + }
  274 + }
  275 + }
  276 +</style>
0 277 \ No newline at end of file
... ...
src/views/project/quest/ReleaseModal.vue 0 → 100644
  1 +<template>
  2 + <a-modal
  3 + :visible="visible"
  4 + title="已通过版本记录"
  5 + width="80%"
  6 + :footer="null"
  7 + @cancel="handleCancel"
  8 + @update:visible="(val) => emit('update:visible', val)"
  9 + >
  10 + <div v-if="loading" class="loading-container">
  11 + <a-spin />
  12 + </div>
  13 + <div v-else>
  14 + <div v-if="tableData.length === 0" class="empty-data">
  15 + <a-empty description="没有找到已通过版本的记录" />
  16 + </div>
  17 + <BasicTable
  18 + v-else
  19 + :dataSource="tableData"
  20 + :columns="columns"
  21 + :pagination="false"
  22 + bordered
  23 + showIndexColumn
  24 + :canResize="false"
  25 + :loading="loading"
  26 + >
  27 + <template #bodyCell="{ column, record }">
  28 + <template v-if="column.key === 'action'">
  29 + <TableAction
  30 + :actions="[
  31 + {
  32 + icon: 'ant-design:eye-outlined',
  33 + tooltip: '查看',
  34 + onClick: handleView.bind(null, record),
  35 + }
  36 + ]"
  37 + />
  38 + </template>
  39 + </template>
  40 + </BasicTable>
  41 + </div>
  42 + </a-modal>
  43 +
  44 + <!-- 查看详情抽屉 -->
  45 + <QuestDrawer @register="registerDrawer" />
  46 +</template>
  47 +
  48 +<script lang="ts" setup name="ReleaseModal">
  49 + import { ref, watch } from 'vue';
  50 + import { getQuestReleaseData } from '/@/api/project/quest';
  51 + import { BasicTable, TableAction } from '/@/components/Table';
  52 + import { useMessage } from '/@/hooks/web/useMessage';
  53 + import { useDrawer } from '/@/components/Drawer';
  54 + import QuestDrawer from './QuestDrawer.vue';
  55 +
  56 + const props = defineProps({
  57 + visible: {
  58 + type: Boolean,
  59 + default: false,
  60 + },
  61 + recordId: {
  62 + type: Number,
  63 + default: undefined,
  64 + },
  65 + });
  66 +
  67 + const emit = defineEmits(['update:visible', 'cancel']);
  68 + const { createMessage } = useMessage();
  69 + const loading = ref(false);
  70 + const tableData = ref<any[]>([]);
  71 +
  72 + // 注册抽屉
  73 + const [registerDrawer, { openDrawer }] = useDrawer();
  74 +
  75 + // 表格列定义
  76 + const columns = [
  77 + {
  78 + title: '标题',
  79 + dataIndex: 'title',
  80 + key: 'title',
  81 + width: 400,
  82 + customHeaderCell: () => ({ style: { color: 'red', fontSize: '16px' } }),
  83 + },
  84 + {
  85 + title: '内容',
  86 + dataIndex: 'contentText',
  87 + key: 'contentText',
  88 + width: 500,
  89 + customRender: ({ text }) => {
  90 + if (!text) return '';
  91 + const div = document.createElement('div');
  92 + div.innerHTML = text;
  93 + const plainText = div.textContent || div.innerText || '';
  94 + return plainText.length > 30 ? plainText.substring(0, 30) + '...' : plainText;
  95 + },
  96 + customHeaderCell: () => ({ style: { color: 'red', fontSize: '16px' } }),
  97 + },
  98 + {
  99 + title: '操作',
  100 + dataIndex: 'action',
  101 + width: 100,
  102 + key: 'action',
  103 + fixed: 'right',
  104 + },
  105 + ];
  106 +
  107 + // 监听visible变化,加载数据
  108 + watch(() => props.visible, async (newVisible) => {
  109 + if (newVisible && props.recordId) {
  110 + await loadReleaseData(props.recordId);
  111 + }
  112 + });
  113 +
  114 + // 加载已通过版本数据
  115 + async function loadReleaseData(id: number) {
  116 + loading.value = true;
  117 + tableData.value = [];
  118 +
  119 + try {
  120 + const response = await getQuestReleaseData(id);
  121 +
  122 + // 简化数据处理逻辑
  123 + if (Array.isArray(response)) {
  124 + // 如果返回的是数组,直接使用
  125 + tableData.value = response;
  126 + } else if (response && Array.isArray(response.items)) {
  127 + // 如果返回的是包含items属性的对象
  128 + tableData.value = response.items;
  129 + } else if (response && Array.isArray(response.records)) {
  130 + // 如果返回的是包含records属性的对象
  131 + tableData.value = response.records;
  132 + } else if (response && typeof response === 'object' && !Array.isArray(response)) {
  133 + // 如果返回的是单个对象
  134 + tableData.value = [response];
  135 + }
  136 +
  137 + // 显示空数据提示
  138 + if (tableData.value.length === 0) {
  139 + createMessage.info('没有找到已通过版本的记录');
  140 + }
  141 + } catch (error) {
  142 + createMessage.error('获取已通过版本数据失败: ' + error.message);
  143 + } finally {
  144 + loading.value = false;
  145 + }
  146 + }
  147 +
  148 + // 取消
  149 + function handleCancel() {
  150 + emit('update:visible', false);
  151 + emit('cancel');
  152 + }
  153 +
  154 + // 查看详情
  155 + function handleView(record) {
  156 + openDrawer(true, {
  157 + record,
  158 + isUpdate: true,
  159 + isView: true, // 标记为查看模式
  160 + });
  161 + }
  162 +</script>
  163 +
  164 +<style lang="less" scoped>
  165 + .loading-container {
  166 + display: flex;
  167 + justify-content: center;
  168 + align-items: center;
  169 + height: 200px;
  170 + }
  171 +
  172 + .empty-data {
  173 + display: flex;
  174 + justify-content: center;
  175 + align-items: center;
  176 + height: 300px;
  177 + background-color: #fafafa;
  178 + border-radius: 4px;
  179 + }
  180 +</style>
  181 +
  182 +<script lang="ts">
  183 +export default {
  184 + name: 'ReleaseModal',
  185 +};
  186 +</script>
0 187 \ No newline at end of file
... ...
src/views/project/quest/index.vue
1 1 <template>
2 2 <div class="quest-container">
3   - <!-- 搜索区域 -->
4   - <!-- <div class="bg-white p-4 mb-4">
5   - <BasicForm @register="registerForm" />
6   - </div> -->
  3 + <!-- 选项卡区域 -->
  4 + <div class="tab-container">
  5 + <a-tabs v-model:activeKey="activeTab" @change="handleTabChange">
  6 + <a-tab-pane key="problems" tab="问题列表"></a-tab-pane>
  7 + <a-tab-pane key="review" tab="审核列表"></a-tab-pane>
  8 + </a-tabs>
  9 + </div>
7 10  
8 11 <!-- 表格区域 -->
9 12 <div class="bg-white">
10   - <BasicTable @register="registerTable">
  13 + <!-- 问题列表 -->
  14 + <BasicTable v-if="activeTab === 'problems'" @register="registerProblemTable">
11 15 <template #toolbar>
12   - <a-button type="primary" @click="handleCreate">
13   - 新建
  16 + <a-button type="primary" @click="handleCalculateAmount" style="margin-right: 8px;" :disabled="checkedRows.length === 0">
  17 + 计算扣款金额
  18 + </a-button>
  19 + <a-button type="primary" @click="handleCreate" style="margin-right: 8px;">
  20 + 新建问题
14 21 </a-button>
  22 + <a-button @click="handleQuestTypeManager">
  23 + 问题类型管理
  24 + </a-button>
  25 + <!-- 已勾选数据提示 -->
  26 + <div v-if="checkedRows.length > 0" class="selected-count">
  27 + 已勾选 {{ checkedRows.length }} 条数据
  28 + </div>
  29 + </template>
  30 +
  31 + <template #bodyCell="{ column, record }">
  32 + <template v-if="column.key === 'action'">
  33 + <TableAction
  34 + :actions="[
  35 + {
  36 + icon: 'ant-design:eye-outlined',
  37 + tooltip: '查看',
  38 + onClick: handleView.bind(null, record),
  39 + },
  40 + {
  41 + icon: 'clarity:note-edit-line',
  42 + tooltip: '编辑',
  43 + onClick: handleEdit.bind(null, record),
  44 + },
  45 + {
  46 + icon: 'ant-design:delete-outlined',
  47 + color: 'error',
  48 + tooltip: '删除',
  49 + popConfirm: {
  50 + title: '是否确认删除?',
  51 + confirm: handleDelete.bind(null, record),
  52 + },
  53 + ifShow: role === ROLE.ADMIN
  54 + },
  55 + ]"
  56 + />
  57 + </template>
  58 + </template>
  59 + </BasicTable>
  60 +
  61 + <!-- 审核列表 -->
  62 + <BasicTable v-if="activeTab === 'review'" @register="registerReviewTable">
  63 + <template #toolbar>
  64 + <!-- 已勾选数据提示 -->
  65 + <div v-if="checkedRows.length > 0" class="selected-count">
  66 + 已勾选 {{ checkedRows.length }} 条数据
  67 + </div>
15 68 </template>
16 69  
17 70 <template #bodyCell="{ column, record }">
... ... @@ -19,6 +72,22 @@
19 72 <TableAction
20 73 :actions="[
21 74 {
  75 + label: '通过',
  76 + color: 'success',
  77 + onClick: handleApprove.bind(null, record),
  78 + ifShow: record.status === 0 && role === ROLE.ADMIN
  79 + },
  80 + {
  81 + label: '不通过',
  82 + color: 'error',
  83 + onClick: handleReject.bind(null, record),
  84 + ifShow: record.status === 0 && role === ROLE.ADMIN
  85 + },
  86 + {
  87 + label: '查看已通过版本',
  88 + onClick: handleViewRelease.bind(null, record),
  89 + },
  90 + {
22 91 icon: 'ant-design:eye-outlined',
23 92 tooltip: '查看',
24 93 onClick: handleView.bind(null, record),
... ... @@ -36,6 +105,7 @@
36 105 title: '是否确认删除?',
37 106 confirm: handleDelete.bind(null, record),
38 107 },
  108 + ifShow: role === ROLE.ADMIN
39 109 },
40 110 ]"
41 111 />
... ... @@ -46,19 +116,107 @@
46 116  
47 117 <!-- 编辑弹窗 -->
48 118 <QuestDrawer @register="registerDrawer" @success="handleSuccess" />
  119 +
  120 + <!-- 拒绝原因弹窗 -->
  121 + <a-modal v-model:visible="rejectModalVisible" title="拒绝原因" @ok="confirmReject">
  122 + <a-textarea v-model:value="rejectReason" placeholder="请输入拒绝原因" :rows="4" />
  123 + </a-modal>
  124 +
  125 + <!-- 已通过版本模态框 -->
  126 + <ReleaseModal
  127 + :visible="releaseModalVisible"
  128 + :recordId="currentReleaseRecordId"
  129 + @update:visible="(val) => releaseModalVisible = val"
  130 + />
  131 +
  132 + <!-- 问题类型管理模态框 -->
  133 + <QuestTypeModal
  134 + :visible="questTypeModalVisible"
  135 + @update:visible="(val) => questTypeModalVisible = val"
  136 + @success="handleQuestTypeSuccess"
  137 + />
  138 +
  139 + <!-- 扣款金额统计弹窗 -->
  140 + <a-modal
  141 + v-model:visible="amountModalVisible"
  142 + title="扣款金额统计"
  143 + :footer="null"
  144 + width="400px"
  145 + >
  146 + <div class="amount-statistics">
  147 + <div class="statistic-item">
  148 + <div class="label">数据总条数:</div>
  149 + <div class="value">{{ deductCountInfo.distinctIdCount }}</div>
  150 + </div>
  151 + <div class="statistic-item">
  152 + <div class="label">合计金额:</div>
  153 + <div class="value amount">{{ getCurrencySymbol(deductCountInfo.isRmb) }}{{ formatAmount(deductCountInfo.sumDeductAmount) }}</div>
  154 + </div>
  155 + </div>
  156 + <div class="modal-footer">
  157 + <a-button type="primary" @click="amountModalVisible = false">确定</a-button>
  158 + </div>
  159 + </a-modal>
49 160 </div>
50 161 </template>
51 162  
52 163 <script lang="ts" setup name="QuestList">
53   - import { onMounted, ref } from 'vue';
  164 + import { onMounted, ref, computed } from 'vue';
54 165 import { BasicTable, useTable, TableAction } from '/@/components/Table';
55 166 import { useDrawer } from '/@/components/Drawer';
56 167 import QuestDrawer from './QuestDrawer.vue';
57   - import { getQuestList, questDelete } from '/@/api/project/quest';
58   - import { columns, searchFormSchema } from './quest.data';
  168 + import ReleaseModal from './ReleaseModal.vue';
  169 + import QuestTypeModal from './QuestTypeModal.vue';
  170 + import { getQuestList, questDelete, setQuestStatus } from '/@/api/project/quest';
  171 + import { columns, reviewColumns, searchFormSchema } from './quest.data';
  172 + import { useMessage } from '/@/hooks/web/useMessage';
  173 + import { defHttp } from '/@/utils/http/axios';
  174 + import { useUserStoreWithOut } from '/@/store/modules/user';
  175 + import {ROLE} from '../order/type.d';
  176 + import { filterFinancialData } from './quest.data';
  177 +
  178 + const { createMessage } = useMessage();
  179 +
  180 + // 选项卡状态
  181 + const activeTab = ref<string>('problems');
  182 +
  183 + // 拒绝原因弹窗状态
  184 + const rejectModalVisible = ref<boolean>(false);
  185 + const rejectReason = ref<string>('');
  186 + const currentRejectRecord = ref<any>(null);
  187 +
  188 + // 已通过版本模态框状态
  189 + const releaseModalVisible = ref<boolean>(false);
  190 + const currentReleaseRecordId = ref<number | undefined>(undefined);
  191 +
  192 + // 问题类型管理模态框状态
  193 + const questTypeModalVisible = ref<boolean>(false);
  194 +
  195 + // 扣款金额统计模态框状态
  196 + const amountModalVisible = ref<boolean>(false);
  197 + const deductCountInfo = ref({
  198 + distinctIdCount: 0,
  199 + sumDeductAmount: 0,
  200 + isRmb: '0' // 默认美元
  201 + });
  202 +
  203 + // 表格选中行数据
  204 + const checkedKeys = ref<(string | number)[]>([]);
  205 + const checkedRows = ref<any[]>([]);
  206 + const userStore = useUserStoreWithOut();
  207 + const user = userStore.getUserInfo;
  208 + const role = computed(() => {
  209 + return user?.roleSmallVO?.code;
  210 + });
59 211  
60   - // 注册表格
61   - const [registerTable, { reload }] = useTable({
  212 + // 表格选择回调函数
  213 + function handleSelectionChange(selectedRowKeys: (string | number)[], selectedRows: any[]) {
  214 + checkedKeys.value = selectedRowKeys;
  215 + checkedRows.value = selectedRows;
  216 + }
  217 +
  218 + // 注册问题列表表格
  219 + const [registerProblemTable, { reload: reloadProblemTable }] = useTable({
62 220 title: '问题列表',
63 221 api: getQuestList,
64 222 columns,
... ... @@ -67,28 +225,101 @@
67 225 schemas: searchFormSchema,
68 226 autoSubmitOnEnter: true,
69 227 },
  228 + beforeFetch: (params) => {
  229 + // 处理日期格式
  230 + if (params.startTime) {
  231 + params.createStartTime = params.startTime.format('YYYY-MM-DD');
  232 + delete params.startTime;
  233 + }
  234 + if (params.endTime) {
  235 + params.createEndTime = params.endTime.format('YYYY-MM-DD');
  236 + delete params.endTime;
  237 + }
  238 + return { ...params, status: 10 };
  239 + },
  240 + // 添加afterFetch钩子,过滤财务专用数据
  241 + afterFetch: (res) => {
  242 + const data = res;
  243 + if (Array.isArray(data)) {
  244 + return filterFinancialData(data);
  245 + }
  246 + if (data && Array.isArray(data.items)) {
  247 + data.items = filterFinancialData(data.items);
  248 + return data;
  249 + }
  250 + return res;
  251 + },
  252 + rowKey: 'id',
70 253 bordered: true,
71   - showIndexColumn: true,
72   - useSearchForm: true,//启用表单搜索
  254 + showIndexColumn: false,
  255 + useSearchForm: true,
  256 + clickToRowSelect: false, // 禁止点击行时选中
  257 + rowSelection: {
  258 + type: 'checkbox',
  259 + onChange: handleSelectionChange,
  260 + preserveSelectedRowKeys: true, // 保留选中的行,即使它们不在当前页面
  261 + },
73 262 });
  263 +
  264 + // 注册审核列表表格
  265 + const [registerReviewTable, { reload: reloadReviewTable }] = useTable({
  266 + title: '审核列表',
  267 + api: getQuestList,
  268 + columns: reviewColumns,
  269 + formConfig: {
  270 + labelWidth: 120,
  271 + schemas: searchFormSchema,
  272 + autoSubmitOnEnter: true,
  273 + },
  274 + beforeFetch: (params) => {
  275 + // 处理日期格式
  276 + if (params.startTime) {
  277 + params.createStartTime = params.startTime.format('YYYY-MM-DD');
  278 + delete params.startTime;
  279 + }
  280 + if (params.endTime) {
  281 + params.createEndTime = params.endTime.format('YYYY-MM-DD');
  282 + delete params.endTime;
  283 + }
  284 + return { ...params, status: 0 };
  285 + },
  286 + // 添加afterFetch钩子,过滤财务专用数据
  287 + afterFetch: (res) => {
  288 + const data = res;
  289 + if (Array.isArray(data)) {
  290 + return filterFinancialData(data);
  291 + }
  292 + if (data && Array.isArray(data.items)) {
  293 + data.items = filterFinancialData(data.items);
  294 + return data;
  295 + }
  296 + return res;
  297 + },
  298 + rowKey: 'id',
  299 + bordered: true,
  300 + showIndexColumn: false,
  301 + useSearchForm: true,
  302 + clickToRowSelect: false, // 禁止点击行时选中
  303 + rowSelection: {
  304 + type: 'checkbox',
  305 + onChange: handleSelectionChange,
  306 + preserveSelectedRowKeys: true, // 保留选中的行,即使它们不在当前页面
  307 + },
  308 + });
  309 +
74 310 // 注册抽屉
75 311 const [registerDrawer, { openDrawer }] = useDrawer();
76 312  
77   - // 表单提交
78   - // async function handleSubmit(values) {
79   - // console.log('表单提交', values);
80   - // // 假设 contentText 是需要转换为数组的字段
81   - // if (typeof values.contentText === 'string') {
82   - // values.contentText = values.contentText.split(',').map(item => item.trim());
83   - // }
84   - // await reload(values);
85   - // return Promise.resolve();
86   - // }
  313 + // 处理选项卡切换
  314 + function handleTabChange(key: string) {
  315 + activeTab.value = key;
  316 + }
87 317  
88 318 // 新建
89 319 function handleCreate() {
90 320 openDrawer(true, {
91 321 isUpdate: false,
  322 + isView: false, // 明确指定不是查看模式
92 323 });
93 324 }
94 325  
... ... @@ -103,9 +334,6 @@
103 334  
104 335 // 编辑
105 336 function handleEdit(record) {
106   - console.log('编辑记录:', record);
107   - console.log('记录ID:', record.id);
108   -
109 337 openDrawer(true, {
110 338 record,
111 339 isUpdate: true,
... ... @@ -116,17 +344,129 @@
116 344 // 删除
117 345 async function handleDelete(record) {
118 346 await questDelete([record.id]);
119   - await reload();
  347 + if (activeTab.value === 'problems') {
  348 + await reloadProblemTable();
  349 + } else {
  350 + await reloadReviewTable();
  351 + }
  352 + }
  353 +
  354 + // 审核通过
  355 + async function handleApprove(record) {
  356 + try {
  357 + await setQuestStatus({ id: record.id, status: 10 });
  358 + await reloadReviewTable();
  359 + } catch (error) {
  360 + createMessage.error('审核失败:' + error.message);
  361 + }
  362 + }
  363 +
  364 + // 审核拒绝(点击触发弹窗)
  365 + function handleReject(record) {
  366 + currentRejectRecord.value = record;
  367 + rejectReason.value = '';
  368 + rejectModalVisible.value = true;
120 369 }
121 370  
122   - // 刷新表格
  371 + // 确认拒绝并提交
  372 + async function confirmReject() {
  373 + if (!rejectReason.value.trim()) {
  374 + createMessage.warning('请输入拒绝原因');
  375 + return;
  376 + }
  377 +
  378 + try {
  379 + await setQuestStatus({
  380 + id: currentRejectRecord.value.id,
  381 + status: 20,
  382 + refuseRemark: rejectReason.value
  383 + });
  384 + rejectModalVisible.value = false;
  385 + await reloadReviewTable();
  386 + } catch (error) {
  387 + createMessage.error('审核失败:' + error.message);
  388 + }
  389 + }
  390 +
  391 + // 表格刷新
123 392 function handleSuccess() {
124   - reload();
  393 + if (activeTab.value === 'problems') {
  394 + reloadProblemTable();
  395 + } else {
  396 + reloadReviewTable();
  397 + }
  398 + }
  399 +
  400 + // 查看已通过版本
  401 + function handleViewRelease(record) {
  402 + currentReleaseRecordId.value = record.id;
  403 + releaseModalVisible.value = true;
  404 + }
  405 +
  406 + // 打开问题类型管理
  407 + function handleQuestTypeManager() {
  408 + questTypeModalVisible.value = true;
  409 + }
  410 +
  411 + // 问题类型管理成功后刷新相关数据
  412 + async function handleQuestTypeSuccess() {
  413 + // 如果有打开的抽屉,通知其刷新问题类型列表
  414 + // 由于没有直接引用QuestDrawer内部方法的方式,我们采用关闭再打开的方式刷新
  415 + const questDrawerInstance = document.querySelector('.quest-drawer');
  416 + if (questDrawerInstance) {
  417 + // 存在打开的抽屉,需要刷新其中的问题类型数据
  418 + createMessage.success('已更新问题类型列表,新建问题时将显示最新数据');
  419 + }
  420 + }
  421 +
  422 + // 格式化金额,保留两位小数
  423 + function formatAmount(amount) {
  424 + if (amount === undefined || amount === null) return '0.00';
  425 + return Number(amount).toFixed(2);
  426 + }
  427 +
  428 + // 获取货币符号
  429 + function getCurrencySymbol(isRmb) {
  430 + return isRmb === '1' ? '¥' : '$';
  431 + }
  432 +
  433 + // 计算扣款金额
  434 + async function handleCalculateAmount() {
  435 + if (checkedKeys.value.length === 0) {
  436 + createMessage.warning('请先选择要计算的数据');
  437 + return;
  438 + }
  439 +
  440 + try {
  441 + // 发送请求获取扣款金额统计
  442 + const res = await defHttp.post({
  443 + url: '/order/erp/quest/getSumDeductAmount_by_ids',
  444 + data: { ids: checkedKeys.value }
  445 + });
  446 +
  447 + // 检查返回结果
  448 + if (res && typeof res === 'object') {
  449 + // 设置统计数据
  450 + deductCountInfo.value = {
  451 + distinctIdCount: res.distinctIdCount || 0,
  452 + sumDeductAmount: res.sumDeductAmount || 0,
  453 + // 获取并处理isRmb值,确保类型一致
  454 + isRmb: (res.isRmb !== undefined && res.isRmb !== null) ? String(res.isRmb) : '0'
  455 + };
  456 +
  457 + // 显示弹窗
  458 + amountModalVisible.value = true;
  459 + } else {
  460 + createMessage.error('获取扣款金额统计失败:返回数据格式不正确');
  461 + }
  462 + } catch (error) {
  463 + createMessage.error('获取扣款金额统计失败:' + (error.message || '未知错误'));
  464 + }
125 465 }
126 466  
127 467 // 页面加载完成后请求数据
128 468 onMounted(async () => {
129   -
  469 + // 默认加载问题列表数据
130 470 });
131 471 </script>
132 472  
... ... @@ -134,8 +474,61 @@
134 474 .quest-container {
135 475 padding: 16px;
136 476  
  477 + .tab-container {
  478 + background-color: white;
  479 + padding: 0 16px;
  480 + margin-bottom: 16px;
  481 + }
  482 +
137 483 :deep(.ant-table-wrapper) {
138 484 padding: 16px;
139 485 }
  486 +
  487 + .selected-count {
  488 + display: inline-block;
  489 + margin-left: 16px;
  490 + padding: 4px 12px;
  491 + background-color: #e6f7ff;
  492 + border: 1px solid #91d5ff;
  493 + border-radius: 4px;
  494 + color: #1890ff;
  495 + font-weight: 500;
  496 + }
  497 + }
  498 +
  499 + // 扣款金额统计弹窗样式
  500 + .amount-statistics {
  501 + padding: 16px;
  502 +
  503 + .statistic-item {
  504 + display: flex;
  505 + align-items: center;
  506 + margin-bottom: 16px;
  507 +
  508 + .label {
  509 + width: 120px;
  510 + text-align: right;
  511 + padding-right: 12px;
  512 + color: #606266;
  513 + font-size: 14px;
  514 + }
  515 +
  516 + .value {
  517 + flex: 1;
  518 + font-size: 16px;
  519 + color: #303133;
  520 + font-weight: 500;
  521 +
  522 + &.amount {
  523 + color: #f56c6c;
  524 + font-size: 18px;
  525 + }
  526 + }
  527 + }
  528 + }
  529 +
  530 + .modal-footer {
  531 + text-align: center;
  532 + margin-top: 24px;
140 533 }
141 534 </style>
... ...
src/views/project/quest/quest.data.tsx
1 1 import { BasicColumn, FormSchema } from '/@/components/Table';
2   - // 表格列定义
3   - export const columns: BasicColumn[] = [
4   - {
5   - title: '标题',
6   - dataIndex: 'title',
7   - key: 'title',
8   - width: 600,
9   - customHeaderCell: () => ({ style: { color: 'red' ,fontSize:'16px'} }), // 让表头变红
  2 +
  3 +import { useUserStoreWithOut } from '/@/store/modules/user';
  4 +import { ROLE } from '../order/type.d';
  5 +import { computed } from 'vue';
  6 +const userStore = useUserStoreWithOut();
  7 +const user=userStore.getUserInfo;
  8 +
  9 +// 基础列定义(包含所有可能的列)
  10 +const baseColumns: BasicColumn[] = [
  11 + {
  12 + title: '标题',
  13 + dataIndex: 'title',
  14 + key: 'title',
  15 + width: 350, // 减小宽度
  16 + customHeaderCell: () => ({ style: { color: 'red' ,fontSize:'16px'} }), // 让表头变红
  17 + },
  18 + {
  19 + title: '问题类型',
  20 + dataIndex: 'questType',
  21 + key: 'questType',
  22 + width: 150, // 设置适当宽度
  23 + customHeaderCell: () => ({ style: { color: 'red', fontSize:'16px' } }), // 让表头变红
  24 + customRender: ({ text, record }) => {
  25 + // 正常显示财务专用信息
  26 + if (record.deductAmount > 0 && record.deductAmount !== undefined) {
  27 + // 根据isRmb字段决定显示$还是¥
  28 + const currencySymbol = record.isRmb === '1' ? '¥' : '$';
  29 +
  30 + // 格式化金额为$xxx.xx或¥xxx.xx格式
  31 + const formattedAmount = typeof record.deductAmount === 'number'
  32 + ? `${currencySymbol}${record.deductAmount.toFixed(2)}`
  33 + : `${currencySymbol}${Number(record.deductAmount).toFixed(2)}`;
  34 +
  35 + return `${text}(${formattedAmount})`;
  36 + }
  37 + return text;
10 38 },
11   - {
12   - title: '内容',
13   - dataIndex: 'contentText',
14   - key: 'contentText',
15   - format: (text) => {
16   - if (!text) return '';
17   - const div = document.createElement('div');
18   - div.innerHTML = text;
19   - const plainText = div.textContent || div.innerText || '';
20   - return plainText.length > 30 ? plainText.substring(0, 30) + '...' : plainText;
21   - },
22   - customHeaderCell: () => ({ style: { color: 'red',fontSize:'16px' } }), // 让表头变红
  39 + },
  40 + {
  41 + title: '内容',
  42 + dataIndex: 'contentText',
  43 + key: 'contentText',
  44 + width: 350, // 减小宽度
  45 + format: (text) => {
  46 + if (!text) return '';
  47 + const div = document.createElement('div');
  48 + div.innerHTML = text;
  49 + const plainText = div.textContent || div.innerText || '';
  50 + return plainText.length > 30 ? plainText.substring(0, 30) + '...' : plainText;
23 51 },
24   - {
25   - title: '操作',
26   - dataIndex: 'action',
27   - width: 160,
28   - key: 'action',
  52 + customHeaderCell: () => ({ style: { color: 'red',fontSize:'16px' } }), // 让表头变红
  53 + },
  54 + {
  55 + title: '状态',
  56 + dataIndex: 'status',
  57 + key: 'status',
  58 + width: 100,
  59 + customRender: ({ text }) => {
  60 + if (text === 10) return '已解决';
  61 + if (text === 20) return '已驳回';
  62 + return '待审核';
29 63 },
30   - ];
  64 + },
  65 + {
  66 + title: '操作',
  67 + dataIndex: 'action',
  68 + width: 160,
  69 + key: 'action',
  70 + fixed: 'right', // 固定在右侧
  71 + },
  72 +];
  73 +const role=computed(() =>{
  74 + return user?.roleSmallVO?.code;
  75 +});
31 76  
  77 +// 问题列表表格列定义(不包含状态列)
  78 +export const columns: BasicColumn[] = [
  79 + baseColumns[0], // 标题
  80 + baseColumns[1], // 问题类型
  81 + baseColumns[2], // 内容
  82 + baseColumns[4], // 操作
  83 +];
32 84  
33   - // 表单配置
34   - export const searchFormSchema: FormSchema[] = [
35   - {
36   - field: 'title',
37   - label: '标题',
38   - component: 'Input',
39   - colProps: { span: 8 },
40   - },
41   - {
42   - field: 'contentText',
43   - label: '内容关键字',
44   - component: 'Input',
45   - colProps: { span: 8 },
  85 +// 审核列表表格列定义(包含状态列)
  86 +export const reviewColumns: BasicColumn[] = [
  87 + baseColumns[0], // 标题
  88 + baseColumns[1], // 问题类型
  89 + baseColumns[2], // 内容
  90 + baseColumns[3], // 状态
  91 + {
  92 + title: '操作',
  93 + dataIndex: 'action',
  94 + width: 320, // 增加宽度以容纳更多按钮
  95 + key: 'action',
  96 + fixed: 'right', // 固定在右侧
  97 + },
  98 +];
  99 +
  100 +// 表单配置
  101 +export const searchFormSchema: FormSchema[] = [
  102 + {
  103 + field: 'title',
  104 + label: '标题',
  105 + component: 'Input',
  106 + colProps: { span: 6 },
  107 + },
  108 + {
  109 + field: 'contentText',
  110 + label: '内容关键字',
  111 + component: 'Input',
  112 + colProps: { span: 6 },
  113 + },
  114 + {
  115 + field: 'questType',
  116 + label: '问题类型',
  117 + component: 'Input',
  118 + colProps: { span: 6 },
  119 + },
  120 + {
  121 + field: `createStartTime`,
  122 + label: `生成开始时间`,
  123 + component: 'DatePicker',
  124 + colProps: {
  125 + span: 6,
46 126 },
47   - {
48   - field: 'createBy',
49   - label: '创建人',
50   - component: 'Input',
51   - colProps: { span: 8 },
  127 + labelWidth: 150,
  128 + },
  129 + {
  130 + field: `createEndTime`,
  131 + label: `生成结束时间`,
  132 + component: 'DatePicker',
  133 + colProps: {
  134 + span: 6,
52 135 },
53   - ];
54 136 \ No newline at end of file
  137 + labelWidth: 150,
  138 + },
  139 +];
  140 +
  141 +// 导出用于过滤财务专用数据的方法
  142 +export const filterFinancialData = (dataList: any[]) => {
  143 + const currentRole = user?.roleSmallVO?.code;
  144 + console.log('角色'+currentRole);
  145 + // 如果用户是管理员或财务,返回所有数据
  146 + if (currentRole === ROLE.ADMIN || currentRole === ROLE.FINANCE) {
  147 + return dataList;
  148 + }
  149 + // 否则过滤掉财务专用的数据
  150 + return dataList.filter(item => item.questType !== '财务专用');
  151 +};
55 152 \ No newline at end of file
... ...
vite.config.ts
... ... @@ -30,12 +30,8 @@ export default defineApplicationConfig({
30 30 },
31 31 },
32 32 '/basic-api/order': {
33   - target: 'http://47.104.8.35:8000',
34   - // target: 'http://localhost:18000',
35   - // target: 'http://39.108.227.113:8000',
36   - // target: 'http://localhost:8000',
37   - // target: 'http://39.108.227.113:3000/mock/35',
38   - // http://39.108.227.113:8000/order/erp/captcha/get_img_captcha_code
  33 + // target: 'http://localhost:18001',
  34 + target: 'http://47.104.8.35:18001',
39 35 changeOrigin: true,
40 36 ws: true,
41 37 rewrite: (path) => path.replace(new RegExp(`^/basic-api`), ''),
... ... @@ -52,11 +48,8 @@ export default defineApplicationConfig({
52 48 // rewrite: (path) => path.replace(new RegExp(`^/basic-api`), ''),
53 49 // },
54 50 '/api/localStorage/upload': {
55   - target: 'http://47.104.8.35:8000',
56   - // target: 'http://localhost:18000',
57   - // target: 'http://39.108.227.113:8000',
58   - // target: '192.168.31.250:18000',
59   - // target: 'http://localhost:8000',
  51 + // target: 'http://localhost:18001',
  52 + target: 'http://47.104.8.35:18001',
60 53 changeOrigin: true,
61 54 ws: true,
62 55 // rewrite: (path) => {
... ...