Skip to content

Commit 7e8e06f

Browse files
committed
feat: 添加取消和催单功能,更新反馈组件和服务
1 parent 7ca4f94 commit 7e8e06f

4 files changed

Lines changed: 224 additions & 28 deletions

File tree

src/assets/css/dashboard/feedback_single.less

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,25 @@
145145
gap: 15px;
146146
align-items: center;
147147
}
148+
.m-actions {
149+
.flex;
150+
align-items: center;
151+
gap: 12px;
152+
flex-wrap: wrap;
153+
justify-content: flex-end;
154+
155+
> * {
156+
margin-left: 0 !important;
157+
}
158+
159+
.u-action-btn {
160+
height: 32px;
161+
min-width: 88px;
162+
padding: 0 12px;
163+
margin-left: 0;
164+
font-weight: 500;
165+
}
166+
}
148167
.u-status {
149168
color: #fff;
150169
padding: 2px 8px;

src/components/dashboard/feedback/single.vue

Lines changed: 123 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -13,35 +13,61 @@
1313
<span class="u-value">{{ subtypes[data.subtype] }}</span>
1414
</div>
1515
</div>
16-
<el-dropdown
17-
v-if="isTeammate && data.status < 12"
18-
split-button
19-
trigger="click"
20-
type="primary"
21-
@click.stop="onDeal"
22-
>
23-
<i class="el-icon-s-tools u-dropdown-icon"></i>
24-
{{ statusText(data.status) }}
25-
<template #dropdown>
26-
<el-dropdown-menu>
27-
<el-dropdown-item @click="handleEdit">
28-
<el-button class="u-btn" type="primary" icon="Edit">编辑</el-button>
29-
</el-dropdown-item>
30-
<el-dropdown-item @click="handleTransfer">
31-
<el-button class="u-btn" type="warning" icon="Right">转交</el-button>
32-
</el-dropdown-item>
33-
<template v-if="data.status === 2">
34-
<el-dropdown-item @click="handleCoordination">
35-
<el-button class="u-btn" type="success" icon="Help">协同</el-button>
16+
<div class="m-actions" v-if="canCancelTicket || canUrgeTicket || (isTeammate && data.status < 12)">
17+
<el-button
18+
v-if="canCancelTicket"
19+
class="u-action-btn is-cancel"
20+
type="danger"
21+
plain
22+
icon="CircleClose"
23+
:loading="closeTicketLoading"
24+
:disabled="closeTicketLoading"
25+
@click.stop="onCancelTicket"
26+
>
27+
取消工单
28+
</el-button>
29+
<el-button
30+
v-if="canUrgeTicket"
31+
class="u-action-btn is-urgent"
32+
type="warning"
33+
plain
34+
icon="Bell"
35+
:loading="urgeTicketLoading"
36+
:disabled="urgeTicketLoading"
37+
@click.stop="onUrgeTicket"
38+
>
39+
催单
40+
</el-button>
41+
<el-dropdown
42+
v-if="isTeammate && data.status < 12"
43+
split-button
44+
trigger="click"
45+
type="primary"
46+
@click.stop="onDeal"
47+
>
48+
<i class="el-icon-s-tools u-dropdown-icon"></i>
49+
{{ statusText(data.status) }}
50+
<template #dropdown>
51+
<el-dropdown-menu>
52+
<el-dropdown-item @click="handleEdit">
53+
<el-button class="u-btn" type="primary" icon="Edit">编辑</el-button>
3654
</el-dropdown-item>
37-
38-
<el-dropdown-item @click="handleClose">
39-
<el-button class="u-btn" type="info" icon="CircleClose">关闭 </el-button>
55+
<el-dropdown-item @click="handleTransfer">
56+
<el-button class="u-btn" type="warning" icon="Right">转交</el-button>
4057
</el-dropdown-item>
41-
</template>
42-
</el-dropdown-menu>
43-
</template>
44-
</el-dropdown>
58+
<template v-if="data.status === 2">
59+
<el-dropdown-item @click="handleCoordination">
60+
<el-button class="u-btn" type="success" icon="Help">协同</el-button>
61+
</el-dropdown-item>
62+
63+
<el-dropdown-item @click="handleClose">
64+
<el-button class="u-btn" type="info" icon="CircleClose">关闭 </el-button>
65+
</el-dropdown-item>
66+
</template>
67+
</el-dropdown-menu>
68+
</template>
69+
</el-dropdown>
70+
</div>
4571
</div>
4672
<div class="m-block m-user">
4773
<div class="u-subblock">
@@ -292,7 +318,7 @@
292318

293319
<script>
294320
import DOMPurify from "dompurify";
295-
import { getFeedback, getFeedbackLog } from "@/service/dashboard/feedback";
321+
import { closeMyFeedback, getFeedback, getFeedbackLog, urgeMyFeedback } from "@/service/dashboard/feedback";
296322
import { getTeammates } from "@/service/dashboard/index";
297323
import feedbackData from "@/assets/data/dashboard/feedback.json";
298324
const { types, subtypes, statusMap, statusColors, statusTypes } = feedbackData;
@@ -348,6 +374,8 @@ export default {
348374
opType: "assign",
349375
statusVisible: false,
350376
isClose: false,
377+
closeTicketLoading: false,
378+
urgeTicketLoading: false,
351379
};
352380
},
353381
computed: {
@@ -390,6 +418,27 @@ export default {
390418
videoAttachments() {
391419
return this.attachments.filter((item) => this.isVideoUrl(item));
392420
},
421+
isTicketOwner() {
422+
const user = User.getInfo();
423+
const ownerId = Number(this.data?.user_id || this.data?.user?.id);
424+
return User.isLogin() && Number(user?.uid) === ownerId;
425+
},
426+
isTicketFinalStatus() {
427+
return [10, 11, 12].includes(Number(this.data?.status));
428+
},
429+
hasUrgedTicket() {
430+
return Boolean(this.data?.is_urgent);
431+
},
432+
canCancelTicket() {
433+
return this.isTicketOwner && !this.isTicketFinalStatus;
434+
},
435+
canUrgeTicket() {
436+
return (
437+
this.isTicketOwner &&
438+
![0, 10, 11, 12].includes(Number(this.data?.status)) &&
439+
!this.hasUrgedTicket
440+
);
441+
},
393442
},
394443
watch: {
395444
id: {
@@ -477,6 +526,52 @@ export default {
477526
this.isClose = true;
478527
this.statusVisible = true;
479528
},
529+
async onCancelTicket() {
530+
if (!this.canCancelTicket || this.closeTicketLoading) return;
531+
532+
try {
533+
const { value } = await this.$prompt("请输入取消工单的备注", "取消工单", {
534+
confirmButtonText: "确定",
535+
cancelButtonText: "取消",
536+
inputPlaceholder: "请输入备注",
537+
inputValidator(value) {
538+
return !!String(value || "").trim() || "请输入备注";
539+
},
540+
});
541+
const remark = String(value || "").trim();
542+
this.closeTicketLoading = true;
543+
await closeMyFeedback(this.id, { remark });
544+
this.$message.success("取消成功");
545+
this.load();
546+
} catch (error) {
547+
if (error !== "cancel" && error !== "close") {
548+
this.$message.error(error?.response?.data?.msg || "取消失败");
549+
}
550+
} finally {
551+
this.closeTicketLoading = false;
552+
}
553+
},
554+
async onUrgeTicket() {
555+
if (!this.canUrgeTicket || this.urgeTicketLoading) return;
556+
557+
try {
558+
await this.$confirm("每个工单只有一次催单机会,确定要催单吗?", "催单", {
559+
confirmButtonText: "确定",
560+
cancelButtonText: "取消",
561+
type: "warning",
562+
});
563+
this.urgeTicketLoading = true;
564+
await urgeMyFeedback(this.id);
565+
this.$message.success("催单成功");
566+
this.load();
567+
} catch (error) {
568+
if (error !== "cancel" && error !== "close") {
569+
this.$message.error(error?.response?.data?.msg || "催单失败");
570+
}
571+
} finally {
572+
this.urgeTicketLoading = false;
573+
}
574+
},
480575
loadLogs() {
481576
this.logLoading = true;
482577
getFeedbackLog(this.id)

src/service/dashboard/feedback.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,23 @@ function getFeedbackLog(id) {
5151
return $next().get(`/api/next2/miscfeedback/item/${id}/history`);
5252
}
5353

54+
/**
55+
* 用户取消自己的反馈工单
56+
* @param {*} id 反馈id
57+
* @param {*} data 取消备注
58+
*/
59+
function closeMyFeedback(id, data) {
60+
return $next().put(`/api/next2/miscfeedback/my/item/${id}/close`, data);
61+
}
62+
63+
/**
64+
* 用户催促自己的反馈工单
65+
* @param {*} id 反馈id
66+
*/
67+
function urgeMyFeedback(id) {
68+
return $next().post(`/api/next2/miscfeedback/item/${id}/urgent`);
69+
}
70+
5471
/**
5572
* 更新反馈
5673
* @param {*} id 反馈id
@@ -97,6 +114,8 @@ export {
97114
getFeedback,
98115
getMiscfeedback,
99116
getFeedbackLog,
117+
closeMyFeedback,
118+
urgeMyFeedback,
100119
updateFeedback,
101120
getTeammates,
102121
assignMiscFeedback,
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import assert from "node:assert/strict";
2+
import fs from "node:fs";
3+
import path from "node:path";
4+
import { fileURLToPath } from "node:url";
5+
6+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
7+
const root = path.resolve(__dirname, "..");
8+
const service = fs.readFileSync(path.join(root, "src/service/dashboard/feedback.js"), "utf8");
9+
const page = fs.readFileSync(path.join(root, "src/components/dashboard/feedback/single.vue"), "utf8");
10+
11+
assert.match(
12+
service,
13+
/function closeMyFeedback\(id,\s*data\)\s*{[\s\S]*miscfeedback\/my\/item\/\$\{id\}\/close[\s\S]*}/,
14+
"service should expose closeMyFeedback using the user close endpoint"
15+
);
16+
17+
assert.match(
18+
service,
19+
/function urgeMyFeedback\(id\)\s*{[\s\S]*\$next\(\)\.post\(`\/api\/next2\/miscfeedback\/item\/\$\{id\}\/urgent`\)[\s\S]*}/,
20+
"service should expose urgeMyFeedback using the urgent endpoint"
21+
);
22+
23+
assert.match(
24+
page,
25+
/<el-button[\s\S]*class="u-action-btn is-cancel"[\s\S]*icon="CircleClose"[\s\S]*[\s\S]*<\/el-button>[\s\S]*<el-dropdown/,
26+
"feedback detail should render cancel ticket button with icon before the deal dropdown"
27+
);
28+
29+
assert.match(
30+
page,
31+
/isTicketOwner\(\)[\s\S]*const user = User\.getInfo\(\)[\s\S]*const ownerId = Number\(this\.data\?\.user_id \|\| this\.data\?\.user\?\.id\)[\s\S]*User\.isLogin\(\) && Number\(user\?\.uid\) === ownerId[\s\S]*isTicketFinalStatus\(\)[\s\S]*\[10,\s*11,\s*12\]\.includes\(Number\(this\.data\?\.status\)\)[\s\S]*canCancelTicket\(\)[\s\S]*this\.isTicketOwner && !this\.isTicketFinalStatus/,
32+
"cancel ticket button should only be available to the ticket owner before processed, closed, or settled statuses"
33+
);
34+
35+
assert.match(
36+
page,
37+
/closeMyFeedback\(this\.id,\s*{\s*remark\s*}\)/,
38+
"cancel ticket should call closeMyFeedback with only remark in the payload"
39+
);
40+
41+
assert.match(
42+
page,
43+
/<el-button[\s\S]*class="u-action-btn is-urgent"[\s\S]*icon="Bell"[\s\S]*[\s\S]*<\/el-button>[\s\S]*<el-dropdown/,
44+
"feedback detail should render urge ticket button with icon before the deal dropdown"
45+
);
46+
47+
assert.match(
48+
fs.readFileSync(path.join(root, "src/assets/css/dashboard/feedback_single.less"), "utf8"),
49+
/\.m-actions[\s\S]*gap:\s*12px[\s\S]*>\s*\*[\s\S]*margin-left:\s*0 !important[\s\S]*\.u-action-btn[\s\S]*height:\s*32px/,
50+
"feedback action buttons should have one consistent gap and stable button sizing"
51+
);
52+
53+
assert.match(
54+
page,
55+
/hasUrgedTicket\(\)[\s\S]*Boolean\(this\.data\?\.is_urgent\)[\s\S]*canUrgeTicket\(\)[\s\S]*this\.isTicketOwner[\s\S]*!\[0,\s*10,\s*11,\s*12\]\.includes\(Number\(this\.data\?\.status\)\)[\s\S]*!this\.hasUrgedTicket/,
56+
"urge ticket button should only be available to the ticket owner after assignment and before final statuses once"
57+
);
58+
59+
assert.match(
60+
page,
61+
/urgeMyFeedback\(this\.id\)/,
62+
"urge ticket should call urgeMyFeedback"
63+
);

0 commit comments

Comments
 (0)