修复工单内容格式与财务页面接口契约
This commit is contained in:
1 parent
90989f8f8a
commit
d330ad01cc
8 files changed
+531
-129
No files matched your search
@@ -312,6 +312,7 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.reply-upload[data-v-3743a20f] {
|
.reply-upload[data-v-3743a20f] {
|
||||||
|
align-self: flex-start;
|
||||||
color: var(--p-text-muted-color);
|
color: var(--p-text-muted-color);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
|||||||
@@ -199,6 +199,12 @@
|
|||||||
flex-basis: 100%;
|
flex-basis: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
.reminder-status[data-v-18fe1f08] {
|
||||||
|
flex-basis: 100%;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--p-text-muted-color);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
.panel.wallet-tabs[data-v-18fe1f08] {
|
.panel.wallet-tabs[data-v-18fe1f08] {
|
||||||
background: 0 0;
|
background: 0 0;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
|||||||
+43
-15
@@ -137,7 +137,13 @@ var je = {
|
|||||||
let _ = useApi(),
|
let _ = useApi(),
|
||||||
x = useRoute(),
|
x = useRoute(),
|
||||||
S = useAuth(),
|
S = useAuth(),
|
||||||
{ rechargeClosed: Qe, load: C } = useSiteConfig_t(),
|
{
|
||||||
|
config: siteConfig,
|
||||||
|
error: siteConfigError,
|
||||||
|
on: configOn,
|
||||||
|
rechargeClosed: Qe,
|
||||||
|
load: C,
|
||||||
|
} = useSiteConfig_t(),
|
||||||
{ enabled: $e, load: w } = useWithdrawAvailability_t(),
|
{ enabled: $e, load: w } = useWithdrawAvailability_t(),
|
||||||
T = ref(20),
|
T = ref(20),
|
||||||
E = ref(!1),
|
E = ref(!1),
|
||||||
@@ -149,6 +155,19 @@ var je = {
|
|||||||
et = ref(!1),
|
et = ref(!1),
|
||||||
M = ref(null),
|
M = ref(null),
|
||||||
N = ref(``);
|
N = ref(``);
|
||||||
|
// 只有站点明确开启时才允许修改;加载、缺项与失败均保留可见说明。
|
||||||
|
const reminderEnabled = computed(() =>
|
||||||
|
configOn(siteConfig.value?.balance_notice_show),
|
||||||
|
);
|
||||||
|
const reminderStatus = computed(() => {
|
||||||
|
if (!siteConfig.value)
|
||||||
|
return siteConfigError.value
|
||||||
|
? `站点配置加载失败,暂时无法修改余额提醒。请刷新后重试。`
|
||||||
|
: `正在加载余额提醒配置…`;
|
||||||
|
if (siteConfig.value.balance_notice_show == null)
|
||||||
|
return `站点尚未提供余额提醒配置,暂时无法修改。`;
|
||||||
|
return reminderEnabled.value ? `` : `站点已关闭余额提醒。`;
|
||||||
|
});
|
||||||
// 账户总览变化时同步余额提醒设置,仅回填本地草稿。
|
// 账户总览变化时同步余额提醒设置,仅回填本地草稿。
|
||||||
function tt() {
|
function tt() {
|
||||||
let e = S.overview;
|
let e = S.overview;
|
||||||
@@ -161,6 +180,7 @@ var je = {
|
|||||||
});
|
});
|
||||||
// 开启余额提醒必须填写正的有限阈值;关闭时发送零阈值,成功后同步账户总览。
|
// 开启余额提醒必须填写正的有限阈值;关闭时发送零阈值,成功后同步账户总览。
|
||||||
async function nt() {
|
async function nt() {
|
||||||
|
if (A.value || !reminderEnabled.value) return;
|
||||||
((j.value = ``), (et.value = !1));
|
((j.value = ``), (et.value = !1));
|
||||||
let e = Number(k.value ?? 0);
|
let e = Number(k.value ?? 0);
|
||||||
if (O.value && (!Number.isFinite(e) || e <= 0)) {
|
if (O.value && (!Number.isFinite(e) || e <= 0)) {
|
||||||
@@ -441,34 +461,34 @@ var je = {
|
|||||||
(S.refreshOverview(), Q(), X());
|
(S.refreshOverview(), Q(), X());
|
||||||
}
|
}
|
||||||
let Et = ref([]),
|
let Et = ref([]),
|
||||||
Dt = ref(0),
|
|
||||||
Ot = ref(1),
|
Ot = ref(1),
|
||||||
kt = ref(!1),
|
kt = ref(!1),
|
||||||
$ = ref(``),
|
$ = ref(``),
|
||||||
freezeRequestVersion = 0;
|
freezeRequestVersion = 0;
|
||||||
// 冻结记录与其他流水遵守同一代次规则,旧请求不能改写结果、错误或加载状态。
|
// 冻结接口返回全量记录,由表格本地分页;旧请求不能改写新结果或加载状态。
|
||||||
async function At() {
|
async function At() {
|
||||||
if (disposed) return;
|
if (disposed) return;
|
||||||
let requestVersion = ++freezeRequestVersion;
|
let requestVersion = ++freezeRequestVersion;
|
||||||
((kt.value = !0), ($.value = ``));
|
((kt.value = !0), ($.value = ``));
|
||||||
try {
|
try {
|
||||||
let e = await _.get(`/account/credit/freeze`, {
|
let e = await _.get(`/account/credit/freeze`);
|
||||||
page: Ot.value,
|
|
||||||
limit: T.value,
|
|
||||||
});
|
|
||||||
if (requestVersion !== freezeRequestVersion) return;
|
if (requestVersion !== freezeRequestVersion) return;
|
||||||
((Et.value = e.list ?? []), (Dt.value = e.count ?? 0));
|
Et.value = e.list ?? [];
|
||||||
|
Ot.value = Math.min(
|
||||||
|
Ot.value,
|
||||||
|
Math.max(1, Math.ceil(Et.value.length / T.value)),
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (requestVersion !== freezeRequestVersion) return;
|
if (requestVersion !== freezeRequestVersion) return;
|
||||||
(($.value = e instanceof Error ? e.message : `加载冻结记录失败`),
|
(($.value = e instanceof Error ? e.message : `加载冻结记录失败`),
|
||||||
(Et.value = []),
|
(Et.value = []),
|
||||||
(Dt.value = 0));
|
(Ot.value = 1));
|
||||||
} finally {
|
} finally {
|
||||||
requestVersion === freezeRequestVersion && (kt.value = !1);
|
requestVersion === freezeRequestVersion && (kt.value = !1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function jt(e) {
|
function jt(e) {
|
||||||
((Ot.value = e.page + 1), (T.value = e.rows), At());
|
((Ot.value = e.page + 1), (T.value = e.rows));
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
// 与共用列表加载器一致:销毁后拒绝新读取,并作废各列表全部在途响应。
|
// 与共用列表加载器一致:销毁后拒绝新读取,并作废各列表全部在途响应。
|
||||||
@@ -699,7 +719,7 @@ var je = {
|
|||||||
"onUpdate:modelValue": (n[2] ||= (e) =>
|
"onUpdate:modelValue": (n[2] ||= (e) =>
|
||||||
isRef(O) ? (O.value = e) : null),
|
isRef(O) ? (O.value = e) : null),
|
||||||
inputId: `credit-reminder-enabled`,
|
inputId: `credit-reminder-enabled`,
|
||||||
disabled: unref(A),
|
disabled: unref(A) || !unref(reminderEnabled),
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
8,
|
8,
|
||||||
@@ -725,7 +745,8 @@ var je = {
|
|||||||
minFractionDigits: 2,
|
minFractionDigits: 2,
|
||||||
maxFractionDigits: 2,
|
maxFractionDigits: 2,
|
||||||
suffix: ` 元`,
|
suffix: ` 元`,
|
||||||
disabled: !unref(O) || unref(A),
|
disabled:
|
||||||
|
!unref(O) || unref(A) || !unref(reminderEnabled),
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
8,
|
8,
|
||||||
@@ -737,13 +758,22 @@ var je = {
|
|||||||
label: `保存`,
|
label: `保存`,
|
||||||
icon: `pi pi-check`,
|
icon: `pi pi-check`,
|
||||||
loading: unref(A),
|
loading: unref(A),
|
||||||
|
disabled: unref(A) || !unref(reminderEnabled),
|
||||||
onClick: nt,
|
onClick: nt,
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
8,
|
8,
|
||||||
[`loading`],
|
[`loading`, `disabled`],
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
|
unref(reminderStatus)
|
||||||
|
? createElementVNode(
|
||||||
|
`p`,
|
||||||
|
{ class: `reminder-status`, role: `status` },
|
||||||
|
toDisplayString(unref(reminderStatus)),
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
: createCommentVNode(``, !0),
|
||||||
unref(j)
|
unref(j)
|
||||||
? (openBlock(),
|
? (openBlock(),
|
||||||
createBlock(
|
createBlock(
|
||||||
@@ -1791,10 +1821,8 @@ var je = {
|
|||||||
{
|
{
|
||||||
value: unref(Et),
|
value: unref(Et),
|
||||||
loading: unref(kt),
|
loading: unref(kt),
|
||||||
lazy: ``,
|
|
||||||
paginator: ``,
|
paginator: ``,
|
||||||
rows: unref(T),
|
rows: unref(T),
|
||||||
totalRecords: unref(Dt),
|
|
||||||
first: (unref(Ot) - 1) * unref(T),
|
first: (unref(Ot) - 1) * unref(T),
|
||||||
rowsPerPageOptions: [20, 50, 100],
|
rowsPerPageOptions: [20, 50, 100],
|
||||||
dataKey: `id`,
|
dataKey: `id`,
|
||||||
|
|||||||
+42
-54
@@ -4,7 +4,6 @@
|
|||||||
*/
|
*/
|
||||||
import { definePageMeta } from "#imports";
|
import { definePageMeta } from "#imports";
|
||||||
import "~/assets/styles/TicketAttachments.DtbJGLkE.css";
|
import "~/assets/styles/TicketAttachments.DtbJGLkE.css";
|
||||||
import "~/assets/styles/RichTextEditor.NkmgPT5m.css";
|
|
||||||
import "~/assets/styles/_id_.C-0--Lx-.css";
|
import "~/assets/styles/_id_.C-0--Lx-.css";
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
key: (e) => e.fullPath,
|
key: (e) => e.fullPath,
|
||||||
@@ -19,7 +18,6 @@ import { ref } from "vue";
|
|||||||
import { withCtx } from "vue";
|
import { withCtx } from "vue";
|
||||||
import { createCommentVNode } from "vue";
|
import { createCommentVNode } from "vue";
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import { isRef } from "vue";
|
|
||||||
import { createElementBlock } from "vue";
|
import { createElementBlock } from "vue";
|
||||||
import { Fragment } from "vue";
|
import { Fragment } from "vue";
|
||||||
import { createElementVNode } from "vue";
|
import { createElementVNode } from "vue";
|
||||||
@@ -38,7 +36,7 @@ import message from "openvue/message";
|
|||||||
import { r as dict_r } from "~/lib/dict.js";
|
import { r as dict_r } from "~/lib/dict.js";
|
||||||
import skeleton from "openvue/skeleton";
|
import skeleton from "openvue/skeleton";
|
||||||
import { n as sanitize_n } from "~/lib/sanitize.js";
|
import { n as sanitize_n } from "~/lib/sanitize.js";
|
||||||
import RichTextEditor from "~/components/RichTextEditor.vue";
|
import textarea from "openvue/textarea";
|
||||||
import { n as ticketAttachments_n } from "~/lib/ticketAttachments.js";
|
import { n as ticketAttachments_n } from "~/lib/ticketAttachments.js";
|
||||||
import TicketAttachments from "~/components/TicketAttachments.vue";
|
import TicketAttachments from "~/components/TicketAttachments.vue";
|
||||||
import TicketNotice from "~/components/TicketNotice.vue";
|
import TicketNotice from "~/components/TicketNotice.vue";
|
||||||
@@ -84,12 +82,10 @@ var le = {
|
|||||||
k = {
|
k = {
|
||||||
class: `time`,
|
class: `time`,
|
||||||
},
|
},
|
||||||
A = [`innerHTML`],
|
|
||||||
j = {
|
j = {
|
||||||
key: 0,
|
key: 0,
|
||||||
class: `message-quote`,
|
class: `message-quote`,
|
||||||
},
|
},
|
||||||
M = [`innerHTML`],
|
|
||||||
N = {
|
N = {
|
||||||
class: `reply-box`,
|
class: `reply-box`,
|
||||||
},
|
},
|
||||||
@@ -98,7 +94,6 @@ var le = {
|
|||||||
class: `reply-quote`,
|
class: `reply-quote`,
|
||||||
role: `status`,
|
role: `status`,
|
||||||
},
|
},
|
||||||
F = [`innerHTML`],
|
|
||||||
I = {
|
I = {
|
||||||
class: `reply-attachments`,
|
class: `reply-attachments`,
|
||||||
},
|
},
|
||||||
@@ -173,6 +168,7 @@ var le = {
|
|||||||
{
|
{
|
||||||
id: 0,
|
id: 0,
|
||||||
content: e.content,
|
content: e.content,
|
||||||
|
content_format: e.content_format,
|
||||||
attachment: e.attachment || [],
|
attachment: e.attachment || [],
|
||||||
create_time: e.create_time,
|
create_time: e.create_time,
|
||||||
type: `Client`,
|
type: `Client`,
|
||||||
@@ -235,13 +231,26 @@ var le = {
|
|||||||
function Ce(e) {
|
function Ce(e) {
|
||||||
e.id > 0 && canOperate.value && (Y.value = e.id);
|
e.id > 0 && canOperate.value && (Y.value = e.id);
|
||||||
}
|
}
|
||||||
// 引用已删除时明确提示;否则优先用服务端引用快照,再从当前对话寻找原文。
|
// 正文和引用共用格式边界:纯文本不经过 HTML 解码,历史富文本继续净化。
|
||||||
|
function contentProps(item) {
|
||||||
|
return item?.content_format === `text`
|
||||||
|
? {
|
||||||
|
textContent: String(item.content ?? ``),
|
||||||
|
style: { whiteSpace: `pre-wrap`, overflowWrap: `anywhere` },
|
||||||
|
}
|
||||||
|
: { innerHTML: sanitize_n(item?.content) };
|
||||||
|
}
|
||||||
|
// 引用快照保留服务端格式;删除提示或缺失提示明确使用纯文本。
|
||||||
function we(e) {
|
function we(e) {
|
||||||
return Number(e.quote_info?.is_deleted) === 1
|
if (Number(e.quote_info?.is_deleted) === 1)
|
||||||
? `该回复已删除`
|
return { content: `该回复已删除`, content_format: `text` };
|
||||||
: e.quote_info?.content ||
|
if (typeof e.quote_info?.content === `string`) return e.quote_info;
|
||||||
Z.value.find((t) => t.id === e.quote_reply_id)?.content ||
|
return (
|
||||||
`原回复 #${e.quote_reply_id}(内容暂不可用)`;
|
Z.value.find((t) => t.id === e.quote_reply_id) || {
|
||||||
|
content: `原回复 #${e.quote_reply_id}(内容暂不可用)`,
|
||||||
|
content_format: `text`,
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// 引用发送者优先来自引用快照,缺失时回查当前对话。
|
// 引用发送者优先来自引用快照,缺失时回查当前对话。
|
||||||
function Te(e) {
|
function Te(e) {
|
||||||
@@ -273,7 +282,7 @@ var le = {
|
|||||||
G.value = !0;
|
G.value = !0;
|
||||||
try {
|
try {
|
||||||
await z.post(`/ticket/${B}/reply`, {
|
await z.post(`/ticket/${B}/reply`, {
|
||||||
content: W.value.trim(),
|
content: W.value,
|
||||||
attachment: q.value.map((e) => e.save_name),
|
attachment: q.value.map((e) => e.save_name),
|
||||||
quote_reply_id: Y.value,
|
quote_reply_id: Y.value,
|
||||||
});
|
});
|
||||||
@@ -320,7 +329,7 @@ var le = {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
onMounted(X),
|
onMounted(X),
|
||||||
// 对话按时间展示并使用公共 HTML 清理与附件解析;引用删除有独立文案,只读工单不开放回复编辑。
|
// 对话遵循服务端内容格式;客户回复使用纯文本,只读工单不开放编辑。
|
||||||
(n, a) => {
|
(n, a) => {
|
||||||
let c = TicketNotice,
|
let c = TicketNotice,
|
||||||
m = NuxtLink,
|
m = NuxtLink,
|
||||||
@@ -328,7 +337,7 @@ var le = {
|
|||||||
v = message,
|
v = message,
|
||||||
y = button,
|
y = button,
|
||||||
S = TicketAttachments,
|
S = TicketAttachments,
|
||||||
C = RichTextEditor;
|
C = textarea;
|
||||||
return (
|
return (
|
||||||
openBlock(),
|
openBlock(),
|
||||||
createElementBlock(`div`, le, [
|
createElementBlock(`div`, le, [
|
||||||
@@ -513,13 +522,10 @@ var le = {
|
|||||||
`div`,
|
`div`,
|
||||||
{
|
{
|
||||||
class: `message-body`,
|
class: `message-body`,
|
||||||
innerHTML: (`sanitizeHtml` in n
|
...contentProps(t),
|
||||||
? n.sanitizeHtml
|
|
||||||
: unref(sanitize_n))(t.content),
|
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
8,
|
16,
|
||||||
A,
|
|
||||||
),
|
),
|
||||||
t.quote_reply_id
|
t.quote_reply_id
|
||||||
? (openBlock(),
|
? (openBlock(),
|
||||||
@@ -535,16 +541,10 @@ var le = {
|
|||||||
createElementVNode(
|
createElementVNode(
|
||||||
`div`,
|
`div`,
|
||||||
{
|
{
|
||||||
innerHTML:
|
...contentProps(we(t)),
|
||||||
(`sanitizeHtml` in n
|
|
||||||
? n.sanitizeHtml
|
|
||||||
: unref(sanitize_n))(
|
|
||||||
we(t),
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
8,
|
16,
|
||||||
M,
|
|
||||||
),
|
),
|
||||||
createVNode(
|
createVNode(
|
||||||
S,
|
S,
|
||||||
@@ -608,15 +608,10 @@ var le = {
|
|||||||
createElementVNode(
|
createElementVNode(
|
||||||
`div`,
|
`div`,
|
||||||
{
|
{
|
||||||
innerHTML: (`sanitizeHtml` in n
|
...contentProps(unref($)),
|
||||||
? n.sanitizeHtml
|
|
||||||
: unref(sanitize_n))(
|
|
||||||
unref($).content,
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
8,
|
16,
|
||||||
F,
|
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
createVNode(y, {
|
createVNode(y, {
|
||||||
@@ -635,27 +630,26 @@ var le = {
|
|||||||
{
|
{
|
||||||
modelValue: unref(W),
|
modelValue: unref(W),
|
||||||
"onUpdate:modelValue": (a[1] ||= (e) =>
|
"onUpdate:modelValue": (a[1] ||= (e) =>
|
||||||
isRef(W) ? (W.value = e) : null),
|
(W.value = e)),
|
||||||
label: `回复内容`,
|
"aria-label": `回复内容`,
|
||||||
"min-height": 140,
|
rows: 6,
|
||||||
|
autoResize: true,
|
||||||
|
fluid: true,
|
||||||
placeholder: `输入回复内容…`,
|
placeholder: `输入回复内容…`,
|
||||||
disabled: !unref(canOperate),
|
disabled: !unref(canOperate) || unref(G),
|
||||||
},
|
},
|
||||||
{
|
null,
|
||||||
"toolbar-end": withCtx(() => [
|
8,
|
||||||
|
[`modelValue`, `disabled`],
|
||||||
|
),
|
||||||
createElementVNode(`label`, L, [
|
createElementVNode(`label`, L, [
|
||||||
(a[3] ||= createElementVNode(
|
(a[3] ||= createElementVNode(
|
||||||
`i`,
|
`i`,
|
||||||
{
|
{ class: `pi pi-paperclip` },
|
||||||
class: `pi pi-paperclip`,
|
|
||||||
},
|
|
||||||
null,
|
null,
|
||||||
-1,
|
-1,
|
||||||
)),
|
)),
|
||||||
(a[4] ||= createTextVNode(
|
(a[4] ||= createTextVNode(`添加附件`, -1)),
|
||||||
`添加附件`,
|
|
||||||
-1,
|
|
||||||
)),
|
|
||||||
createElementVNode(
|
createElementVNode(
|
||||||
`input`,
|
`input`,
|
||||||
{
|
{
|
||||||
@@ -673,12 +667,6 @@ var le = {
|
|||||||
_e,
|
_e,
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
]),
|
|
||||||
_: 1,
|
|
||||||
},
|
|
||||||
8,
|
|
||||||
[`modelValue`, `disabled`],
|
|
||||||
),
|
|
||||||
createElementVNode(`div`, I, [
|
createElementVNode(`div`, I, [
|
||||||
unref(J)
|
unref(J)
|
||||||
? (openBlock(),
|
? (openBlock(),
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
/**
|
/**
|
||||||
* 创建工单:加载类型/部门及可选实例,上传附件后提交标题、富文本内容和关联实例;选项未就绪或附件仍上传时不提交。
|
* 创建工单:加载实际部门及可选实例,上传附件后提交标题、纯文本内容和关联实例;选项未就绪或附件仍上传时不提交。
|
||||||
*/
|
*/
|
||||||
import "~/assets/styles/RichTextEditor.NkmgPT5m.css";
|
|
||||||
import "~/assets/styles/create.gX6gpD00.css";
|
import "~/assets/styles/create.gX6gpD00.css";
|
||||||
import { toDisplayString } from "vue";
|
import { toDisplayString } from "vue";
|
||||||
import { renderList } from "vue";
|
import { renderList } from "vue";
|
||||||
@@ -32,8 +31,7 @@ import message from "openvue/message";
|
|||||||
import radiobutton from "openvue/radiobutton";
|
import radiobutton from "openvue/radiobutton";
|
||||||
import inputtext from "openvue/inputtext";
|
import inputtext from "openvue/inputtext";
|
||||||
import { n as product_name_n } from "~/lib/product-name.js";
|
import { n as product_name_n } from "~/lib/product-name.js";
|
||||||
import select from "openvue/select";
|
import textarea from "openvue/textarea";
|
||||||
import RichTextEditor from "~/components/RichTextEditor.vue";
|
|
||||||
import TicketNotice from "~/components/TicketNotice.vue";
|
import TicketNotice from "~/components/TicketNotice.vue";
|
||||||
import chip from "openvue/chip";
|
import chip from "openvue/chip";
|
||||||
import multiselect from "openvue/multiselect";
|
import multiselect from "openvue/multiselect";
|
||||||
@@ -56,10 +54,6 @@ var le = {
|
|||||||
w = {
|
w = {
|
||||||
class: `field`,
|
class: `field`,
|
||||||
},
|
},
|
||||||
T = {
|
|
||||||
key: 2,
|
|
||||||
class: `field`,
|
|
||||||
},
|
|
||||||
E = {
|
E = {
|
||||||
class: `field`,
|
class: `field`,
|
||||||
},
|
},
|
||||||
@@ -110,12 +104,10 @@ var le = {
|
|||||||
let b = useApi(),
|
let b = useApi(),
|
||||||
z = useRouter(),
|
z = useRouter(),
|
||||||
B = ref([]),
|
B = ref([]),
|
||||||
V = ref([]),
|
|
||||||
H = ref([]),
|
H = ref([]),
|
||||||
U = reactive({
|
U = reactive({
|
||||||
title: ``,
|
title: ``,
|
||||||
ticket_type_id: null,
|
ticket_type_id: null,
|
||||||
admin_role_id: null,
|
|
||||||
host_ids: [],
|
host_ids: [],
|
||||||
content: ``,
|
content: ``,
|
||||||
}),
|
}),
|
||||||
@@ -172,17 +164,14 @@ var le = {
|
|||||||
function ge(e) {
|
function ge(e) {
|
||||||
W.value || X.value.splice(e, 1);
|
W.value || X.value.splice(e, 1);
|
||||||
}
|
}
|
||||||
// 类型/部门加载成功后再开放提交;实例可选列表分页读取,最多 100 页,实例查询失败不阻止无关联工单创建。
|
// 后端以 ticket_type_id 决定部门;实例查询失败不阻止无关联工单创建。
|
||||||
async function Q() {
|
async function Q() {
|
||||||
if (isCurrent() && !K.value) {
|
if (isCurrent() && !K.value) {
|
||||||
((K.value = !0), (q.value = !1), (G.value = ``));
|
((K.value = !0), (q.value = !1), (G.value = ``));
|
||||||
try {
|
try {
|
||||||
let [e, t] = await Promise.all([
|
let e = await b.get(`/ticket/type`);
|
||||||
b.get(`/ticket/type`),
|
|
||||||
b.get(`/ticket/department`),
|
|
||||||
]);
|
|
||||||
if (!isCurrent()) return;
|
if (!isCurrent()) return;
|
||||||
((B.value = e.list ?? []), (V.value = t.list ?? []));
|
B.value = e.list ?? [];
|
||||||
try {
|
try {
|
||||||
let e = [],
|
let e = [],
|
||||||
t = 1,
|
t = 1,
|
||||||
@@ -203,9 +192,7 @@ var le = {
|
|||||||
if (!isCurrent()) return;
|
if (!isCurrent()) return;
|
||||||
H.value = [];
|
H.value = [];
|
||||||
}
|
}
|
||||||
((U.ticket_type_id = B.value[0]?.id ?? null),
|
((U.ticket_type_id = B.value[0]?.id ?? null), (q.value = !0));
|
||||||
(U.admin_role_id = V.value[0]?.id ?? null),
|
|
||||||
(q.value = !0));
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!isCurrent()) return;
|
if (!isCurrent()) return;
|
||||||
G.value =
|
G.value =
|
||||||
@@ -230,9 +217,8 @@ var le = {
|
|||||||
try {
|
try {
|
||||||
let e = await b.post(`/ticket`, {
|
let e = await b.post(`/ticket`, {
|
||||||
title: U.title.trim(),
|
title: U.title.trim(),
|
||||||
content: U.content.trim(),
|
content: U.content,
|
||||||
ticket_type_id: U.ticket_type_id,
|
ticket_type_id: U.ticket_type_id,
|
||||||
admin_role_id: U.admin_role_id,
|
|
||||||
host_ids: U.host_ids,
|
host_ids: U.host_ids,
|
||||||
attachment: X.value.map((e) => e.save_name),
|
attachment: X.value.map((e) => e.save_name),
|
||||||
});
|
});
|
||||||
@@ -248,16 +234,15 @@ var le = {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
onMounted(Q),
|
onMounted(Q),
|
||||||
// 类型/部门选项就绪且附件上传完成后才可创建;富文本编辑器与本地附件草稿共同组成提交内容。
|
// 部门选项就绪且附件上传完成后才可创建;正文按客户纯文本契约原样提交。
|
||||||
(n, i) => {
|
(n, i) => {
|
||||||
let s = TicketNotice,
|
let s = TicketNotice,
|
||||||
c = button,
|
c = button,
|
||||||
m = radiobutton,
|
m = radiobutton,
|
||||||
_ = inputtext,
|
_ = inputtext,
|
||||||
y = select,
|
|
||||||
b = chip,
|
b = chip,
|
||||||
z = multiselect,
|
z = multiselect,
|
||||||
J = RichTextEditor,
|
J = textarea,
|
||||||
Y = message,
|
Y = message,
|
||||||
_e = NuxtLink;
|
_e = NuxtLink;
|
||||||
return (
|
return (
|
||||||
@@ -303,7 +288,7 @@ var le = {
|
|||||||
(i[5] ||= createElementVNode(
|
(i[5] ||= createElementVNode(
|
||||||
`legend`,
|
`legend`,
|
||||||
null,
|
null,
|
||||||
`工单类型`,
|
`工单部门`,
|
||||||
-1,
|
-1,
|
||||||
)),
|
)),
|
||||||
(openBlock(!0),
|
(openBlock(!0),
|
||||||
@@ -377,36 +362,6 @@ var le = {
|
|||||||
[`modelValue`],
|
[`modelValue`],
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
unref(V).length > 1
|
|
||||||
? (openBlock(),
|
|
||||||
createElementBlock(`div`, T, [
|
|
||||||
(i[7] ||= createElementVNode(
|
|
||||||
`label`,
|
|
||||||
{
|
|
||||||
for: `department`,
|
|
||||||
},
|
|
||||||
`服务部门`,
|
|
||||||
-1,
|
|
||||||
)),
|
|
||||||
createVNode(
|
|
||||||
y,
|
|
||||||
{
|
|
||||||
"input-id": `department`,
|
|
||||||
modelValue: unref(U).admin_role_id,
|
|
||||||
"onUpdate:modelValue": (i[2] ||= (e) =>
|
|
||||||
(unref(U).admin_role_id = e)),
|
|
||||||
options: unref(V),
|
|
||||||
optionLabel: `name`,
|
|
||||||
optionValue: `id`,
|
|
||||||
disabled: !unref(q),
|
|
||||||
fluid: ``,
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
8,
|
|
||||||
[`modelValue`, `options`, `disabled`],
|
|
||||||
),
|
|
||||||
]))
|
|
||||||
: createCommentVNode(``, !0),
|
|
||||||
createElementVNode(`div`, E, [
|
createElementVNode(`div`, E, [
|
||||||
(i[8] ||= createElementVNode(
|
(i[8] ||= createElementVNode(
|
||||||
`label`,
|
`label`,
|
||||||
@@ -517,7 +472,9 @@ var le = {
|
|||||||
modelValue: unref(U).content,
|
modelValue: unref(U).content,
|
||||||
"onUpdate:modelValue": (i[4] ||= (e) =>
|
"onUpdate:modelValue": (i[4] ||= (e) =>
|
||||||
(unref(U).content = e)),
|
(unref(U).content = e)),
|
||||||
label: `工单内容`,
|
rows: 8,
|
||||||
|
autoResize: true,
|
||||||
|
fluid: true,
|
||||||
placeholder: `请描述问题、复现步骤和报错信息`,
|
placeholder: `请描述问题、复现步骤和报错信息`,
|
||||||
disabled: unref(W),
|
disabled: unref(W),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
*/
|
*/
|
||||||
import { definePageMeta } from "#imports";
|
import { definePageMeta } from "#imports";
|
||||||
import "~/assets/styles/TicketAttachments.DtbJGLkE.css";
|
import "~/assets/styles/TicketAttachments.DtbJGLkE.css";
|
||||||
import "~/assets/styles/RichTextEditor.NkmgPT5m.css";
|
|
||||||
import "~/assets/styles/_id_.C-0--Lx-.css";
|
import "~/assets/styles/_id_.C-0--Lx-.css";
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
key: (route) => route.fullPath,
|
key: (route) => route.fullPath,
|
||||||
|
|||||||
@@ -0,0 +1,423 @@
|
|||||||
|
/** 工单内容/部门与账务分页/能力开关的契约回归;只控制本地 HTTP 边界。 */
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { test } from "node:test";
|
||||||
|
import {
|
||||||
|
appOrigin,
|
||||||
|
backendOrigin,
|
||||||
|
eventually,
|
||||||
|
login,
|
||||||
|
scenario,
|
||||||
|
} from "./helpers/console-browser.mjs";
|
||||||
|
|
||||||
|
async function reply(route, data) {
|
||||||
|
return route.fulfill({
|
||||||
|
json: { status: 200, msg: "", data },
|
||||||
|
headers: { "Access-Control-Allow-Origin": appOrigin },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function financeLists(page) {
|
||||||
|
const paths = [
|
||||||
|
"/transaction",
|
||||||
|
"/credit",
|
||||||
|
"/withdraw",
|
||||||
|
"/order",
|
||||||
|
"/refund/pending/amount",
|
||||||
|
"/account/credit/freeze",
|
||||||
|
];
|
||||||
|
await page.route(
|
||||||
|
(url) =>
|
||||||
|
url.origin === backendOrigin &&
|
||||||
|
paths.includes(url.pathname.replace("/console/v1", "")),
|
||||||
|
(route) =>
|
||||||
|
reply(
|
||||||
|
route,
|
||||||
|
new URL(route.request().url()).pathname.endsWith(
|
||||||
|
"/refund/pending/amount",
|
||||||
|
)
|
||||||
|
? { amount: "0.00" }
|
||||||
|
: { list: [], count: 0, total_amount: "0.00" },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ticket(content, replies = []) {
|
||||||
|
return {
|
||||||
|
id: 23,
|
||||||
|
ticket_num: "LOCAL-23",
|
||||||
|
title: "内容契约验收",
|
||||||
|
content,
|
||||||
|
content_format: "text",
|
||||||
|
attachment: [],
|
||||||
|
replies,
|
||||||
|
status: "待回复",
|
||||||
|
can_operate: 1,
|
||||||
|
create_time: 1730000000,
|
||||||
|
username: "audit-user",
|
||||||
|
client_id: 1,
|
||||||
|
color: "#777",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ticket creation submits literal text and its one effective department",
|
||||||
|
{ timeout: 30000 },
|
||||||
|
(t) =>
|
||||||
|
scenario(t, { loginMode: "password-only" }, async (page) => {
|
||||||
|
const content = " <b>用户代码</b>\n 第二行 <tag>\n尾行 ";
|
||||||
|
const writes = [];
|
||||||
|
await page.route(
|
||||||
|
(url) =>
|
||||||
|
url.origin === backendOrigin &&
|
||||||
|
url.pathname.startsWith("/console/v1/ticket"),
|
||||||
|
async (route) => {
|
||||||
|
const request = route.request();
|
||||||
|
const path = new URL(request.url()).pathname.replace(
|
||||||
|
"/console/v1",
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
if (path === "/ticket/config")
|
||||||
|
return reply(route, { ticket_notice_open: 0 });
|
||||||
|
if (["/ticket/type", "/ticket/department"].includes(path))
|
||||||
|
return reply(route, {
|
||||||
|
list: [
|
||||||
|
{ id: 1, name: "账务支持" },
|
||||||
|
{ id: 2, name: "网络支持" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
if (path === "/ticket" && request.method() === "POST") {
|
||||||
|
writes.push(request.postDataJSON());
|
||||||
|
return reply(route, { id: 23 });
|
||||||
|
}
|
||||||
|
if (path === "/ticket/23")
|
||||||
|
return reply(route, { ticket: ticket(content) });
|
||||||
|
return route.fallback();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await login(page, "/tickets/create");
|
||||||
|
await page.getByRole("radio", { name: "网络支持", exact: true }).check();
|
||||||
|
await page
|
||||||
|
.getByRole("textbox", { name: "工单标题", exact: true })
|
||||||
|
.fill("内容契约验收");
|
||||||
|
await page
|
||||||
|
.getByRole("textbox", { name: "工单内容", exact: true })
|
||||||
|
.fill(content);
|
||||||
|
await page.getByRole("button", { name: "创建工单", exact: true }).click();
|
||||||
|
await eventually(
|
||||||
|
() => writes,
|
||||||
|
(rows) => rows.length === 1,
|
||||||
|
"工单必须提交一次",
|
||||||
|
);
|
||||||
|
assert.equal(writes[0].ticket_type_id, 2);
|
||||||
|
assert.equal(writes[0].content, content);
|
||||||
|
assert.equal(Object.hasOwn(writes[0], "admin_role_id"), false);
|
||||||
|
await page.waitForURL((url) => url.pathname === "/tickets/23");
|
||||||
|
assert.equal(
|
||||||
|
await page.locator(".message-body").first().textContent(),
|
||||||
|
content,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
effectiveDepartment: 2,
|
||||||
|
contentKeptLiteral: true,
|
||||||
|
createdTickets: 1,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"ticket text, historical HTML, reply snapshots and reply drafts retain their declared format",
|
||||||
|
{ timeout: 30000 },
|
||||||
|
(t) =>
|
||||||
|
scenario(t, { loginMode: "password-only" }, async (page) => {
|
||||||
|
const initial = " <b>字面代码</b>\n 空格 <tag>";
|
||||||
|
const quoted = "<i>字面引用</i>\n下一行";
|
||||||
|
const replies = [
|
||||||
|
{
|
||||||
|
id: 11,
|
||||||
|
content: quoted,
|
||||||
|
content_format: "text",
|
||||||
|
type: "Client",
|
||||||
|
client_name: "audit-user",
|
||||||
|
create_time: 1730000001,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 12,
|
||||||
|
content: "<strong>历史客服排版</strong>",
|
||||||
|
content_format: "html",
|
||||||
|
type: "Admin",
|
||||||
|
admin_name: "本地客服",
|
||||||
|
create_time: 1730000002,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 13,
|
||||||
|
content: "快照回复",
|
||||||
|
content_format: "text",
|
||||||
|
type: "Client",
|
||||||
|
client_name: "audit-user",
|
||||||
|
create_time: 1730000003,
|
||||||
|
quote_reply_id: 11,
|
||||||
|
quote_info: {
|
||||||
|
content: quoted,
|
||||||
|
content_format: "text",
|
||||||
|
sender_name: "audit-user",
|
||||||
|
attachment: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const data = ticket(initial, replies);
|
||||||
|
const writes = [];
|
||||||
|
await page.route(
|
||||||
|
(url) =>
|
||||||
|
url.origin === backendOrigin &&
|
||||||
|
url.pathname.startsWith("/console/v1/ticket/"),
|
||||||
|
async (route) => {
|
||||||
|
const request = route.request();
|
||||||
|
const path = new URL(request.url()).pathname.replace(
|
||||||
|
"/console/v1",
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
if (path === "/ticket/config")
|
||||||
|
return reply(route, { ticket_notice_open: 0 });
|
||||||
|
if (path === "/ticket/23") return reply(route, { ticket: data });
|
||||||
|
if (path === "/ticket/23/reply") {
|
||||||
|
const body = request.postDataJSON();
|
||||||
|
writes.push(body);
|
||||||
|
data.replies.push({
|
||||||
|
id: 14,
|
||||||
|
...body,
|
||||||
|
content_format: "text",
|
||||||
|
type: "Client",
|
||||||
|
client_name: "audit-user",
|
||||||
|
create_time: 1730000004,
|
||||||
|
quote_info: {
|
||||||
|
content: quoted,
|
||||||
|
content_format: "text",
|
||||||
|
sender_name: "audit-user",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return reply(route, {});
|
||||||
|
}
|
||||||
|
return route.fallback();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await login(page, "/tickets/23");
|
||||||
|
const messages = page.locator(".thread .message");
|
||||||
|
await messages.nth(3).waitFor();
|
||||||
|
const first = messages.first().locator(".message-body");
|
||||||
|
assert.equal(await first.textContent(), initial);
|
||||||
|
assert.equal(await first.locator("b").count(), 0);
|
||||||
|
assert.equal(
|
||||||
|
await first.evaluate((node) => getComputedStyle(node).whiteSpace),
|
||||||
|
"pre-wrap",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
await messages.nth(2).locator(".message-body strong").textContent(),
|
||||||
|
"历史客服排版",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
await messages.nth(3).locator(".message-quote div").textContent(),
|
||||||
|
quoted,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
await messages.nth(3).locator(".message-quote i").count(),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
await messages
|
||||||
|
.nth(1)
|
||||||
|
.getByRole("button", { name: "引用", exact: true })
|
||||||
|
.click();
|
||||||
|
assert.equal(
|
||||||
|
await page.locator(".reply-quote > div > div").textContent(),
|
||||||
|
quoted,
|
||||||
|
);
|
||||||
|
const draft = " <script>字面错误日志</script>\n 保留缩进 ";
|
||||||
|
await page
|
||||||
|
.getByRole("textbox", { name: "回复内容", exact: true })
|
||||||
|
.fill(draft);
|
||||||
|
await page.getByRole("button", { name: "提交回复", exact: true }).click();
|
||||||
|
await eventually(
|
||||||
|
() => writes,
|
||||||
|
(rows) => rows.length === 1,
|
||||||
|
"回复必须提交一次",
|
||||||
|
);
|
||||||
|
assert.equal(writes[0].content, draft);
|
||||||
|
assert.equal(writes[0].quote_reply_id, 11);
|
||||||
|
await messages.nth(4).waitFor();
|
||||||
|
assert.equal(
|
||||||
|
await messages.nth(4).locator(".message-body").textContent(),
|
||||||
|
draft,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
textAndSnapshotsKeptLiteral: true,
|
||||||
|
historicalHtmlRendered: true,
|
||||||
|
replyKeptWhitespace: true,
|
||||||
|
quotedReply: 11,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"full freeze lists paginate locally without duplicate rows or extra reads",
|
||||||
|
{ timeout: 30000 },
|
||||||
|
(t) =>
|
||||||
|
scenario(t, { loginMode: "password-only" }, async (page) => {
|
||||||
|
await financeLists(page);
|
||||||
|
let reads = 0;
|
||||||
|
const list = Array.from({ length: 21 }, (_, index) => ({
|
||||||
|
id: index + 1,
|
||||||
|
amount: "10.00",
|
||||||
|
client_notes: `冻结记录-${index + 1}`,
|
||||||
|
create_time: 1730000000,
|
||||||
|
}));
|
||||||
|
await page.route(
|
||||||
|
(url) =>
|
||||||
|
url.origin === backendOrigin &&
|
||||||
|
url.pathname === "/console/v1/account/credit/freeze",
|
||||||
|
(route) => {
|
||||||
|
reads++;
|
||||||
|
return reply(route, { list, count: list.length });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await login(page, "/finance?tab=freeze");
|
||||||
|
const panel = page.getByRole("tabpanel").filter({ visible: true });
|
||||||
|
await panel.getByText("冻结记录-1", { exact: true }).waitFor();
|
||||||
|
assert.equal(await panel.locator("tbody tr").count(), 20);
|
||||||
|
await panel.locator(".p-paginator-next").click();
|
||||||
|
await panel.getByText("冻结记录-21", { exact: true }).waitFor();
|
||||||
|
assert.equal(await panel.locator("tbody tr").count(), 1);
|
||||||
|
assert.equal(
|
||||||
|
await panel.getByText("冻结记录-1", { exact: true }).count(),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
assert.equal(reads, 1);
|
||||||
|
return { firstPageRows: 20, secondPageRows: 1, totalReads: reads };
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const mode of ["enabled", "disabled", "missing", "failure"]) {
|
||||||
|
test(
|
||||||
|
`balance reminders follow the ${mode} site capability`,
|
||||||
|
{ timeout: 30000 },
|
||||||
|
(t) =>
|
||||||
|
scenario(t, { loginMode: "password-only" }, async (page) => {
|
||||||
|
const writes = [];
|
||||||
|
await financeLists(page);
|
||||||
|
// 配置失败场景先经真实表单建立会话,再整页刷新触发新的公共配置读取。
|
||||||
|
if (mode === "failure") await login(page, "/finance");
|
||||||
|
await page.route(
|
||||||
|
(url) =>
|
||||||
|
url.origin === backendOrigin &&
|
||||||
|
url.pathname === "/console/v1/common",
|
||||||
|
async (route) => {
|
||||||
|
if (mode === "failure")
|
||||||
|
return route.fulfill({
|
||||||
|
json: { status: 400, msg: "本地配置暂不可用", data: {} },
|
||||||
|
headers: { "Access-Control-Allow-Origin": appOrigin },
|
||||||
|
});
|
||||||
|
const response = await route.fetch();
|
||||||
|
const payload = await response.json();
|
||||||
|
if (mode === "missing") delete payload.data.balance_notice_show;
|
||||||
|
else payload.data.balance_notice_show = mode === "enabled" ? 1 : 0;
|
||||||
|
return route.fulfill({ response, json: payload });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await page.route(
|
||||||
|
(url) =>
|
||||||
|
url.origin === backendOrigin &&
|
||||||
|
url.pathname === "/console/v1/account/credit/remind",
|
||||||
|
(route) => {
|
||||||
|
writes.push(route.request().postDataJSON());
|
||||||
|
return reply(route, {});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (mode === "failure") await page.goto(`${appOrigin}/finance`);
|
||||||
|
else await login(page, "/finance");
|
||||||
|
const panel = page.locator(".reminder-panel");
|
||||||
|
const save = panel.getByRole("button", { name: "保存", exact: true });
|
||||||
|
await save.waitFor();
|
||||||
|
if (mode === "enabled") {
|
||||||
|
await panel.locator("#credit-reminder-enabled").check();
|
||||||
|
await panel.locator("#credit-reminder-amount").fill("25.00");
|
||||||
|
await save.click();
|
||||||
|
await eventually(
|
||||||
|
() => writes,
|
||||||
|
(rows) => rows.length === 1,
|
||||||
|
"开启能力后可保存提醒",
|
||||||
|
);
|
||||||
|
assert.deepEqual(writes[0], {
|
||||||
|
credit_remind: 1,
|
||||||
|
credit_remind_amount: 25,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
assert.equal(await save.isDisabled(), true);
|
||||||
|
assert.equal(
|
||||||
|
await panel.locator("#credit-reminder-enabled").isDisabled(),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
await panel.locator("#credit-reminder-amount").isDisabled(),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert.equal(await panel.getByRole("status").isVisible(), true);
|
||||||
|
assert.equal(writes.length, 0);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
capability: mode,
|
||||||
|
saved: mode === "enabled",
|
||||||
|
writes: writes.length,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test(
|
||||||
|
"balance reminders stay disabled while a failed startup configuration is being reloaded",
|
||||||
|
{ timeout: 30000 },
|
||||||
|
(t) =>
|
||||||
|
scenario(t, { loginMode: "password-only" }, async (page) => {
|
||||||
|
await financeLists(page);
|
||||||
|
await login(page, "/finance");
|
||||||
|
let reads = 0;
|
||||||
|
let release;
|
||||||
|
const held = new Promise((resolve) => {
|
||||||
|
release = resolve;
|
||||||
|
});
|
||||||
|
await page.route(
|
||||||
|
(url) =>
|
||||||
|
url.origin === backendOrigin && url.pathname === "/console/v1/common",
|
||||||
|
async (route) => {
|
||||||
|
if (++reads === 1)
|
||||||
|
return route.fulfill({
|
||||||
|
json: { status: 400, msg: "本地首次配置读取失败", data: {} },
|
||||||
|
headers: { "Access-Control-Allow-Origin": appOrigin },
|
||||||
|
});
|
||||||
|
await held;
|
||||||
|
const response = await route.fetch();
|
||||||
|
const payload = await response.json();
|
||||||
|
payload.data.balance_notice_show = 1;
|
||||||
|
return route.fulfill({ response, json: payload });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await page.goto(`${appOrigin}/finance`);
|
||||||
|
const panel = page.locator(".reminder-panel");
|
||||||
|
const save = panel.getByRole("button", { name: "保存", exact: true });
|
||||||
|
await save.waitFor();
|
||||||
|
assert.equal(await save.isDisabled(), true);
|
||||||
|
assert.equal(
|
||||||
|
await panel.locator("#credit-reminder-enabled").isDisabled(),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert.equal(await panel.getByRole("status").isVisible(), true);
|
||||||
|
release();
|
||||||
|
await eventually(
|
||||||
|
() => save.isEnabled(),
|
||||||
|
Boolean,
|
||||||
|
"配置确认开启后才允许保存",
|
||||||
|
);
|
||||||
|
return { pendingCapabilityDisabled: true, enabledAfterReload: true };
|
||||||
|
} finally {
|
||||||
|
release();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
@@ -272,7 +272,7 @@ test(
|
|||||||
await page
|
await page
|
||||||
.getByRole("heading", { name: "审计工单2", exact: true })
|
.getByRole("heading", { name: "审计工单2", exact: true })
|
||||||
.waitFor();
|
.waitFor();
|
||||||
assert.equal((await editor.innerText()).trim(), "");
|
assert.equal(await editor.inputValue(), "");
|
||||||
await editor.fill("第二张工单的回复");
|
await editor.fill("第二张工单的回复");
|
||||||
await page.getByRole("button", { name: "提交回复", exact: true }).click();
|
await page.getByRole("button", { name: "提交回复", exact: true }).click();
|
||||||
await eventually(
|
await eventually(
|
||||||
@@ -289,7 +289,7 @@ test(
|
|||||||
await page
|
await page
|
||||||
.getByRole("button", { name: "关闭工单", exact: true })
|
.getByRole("button", { name: "关闭工单", exact: true })
|
||||||
.waitFor({ state: "hidden" });
|
.waitFor({ state: "hidden" });
|
||||||
assert.equal(await editor.getAttribute("contenteditable"), "false");
|
assert.equal(await editor.isEditable(), false);
|
||||||
assert.equal(await page.locator("#reply-files").isDisabled(), true);
|
assert.equal(await page.locator("#reply-files").isDisabled(), true);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
await page
|
await page
|
||||||
@@ -404,7 +404,7 @@ for (const [routePath, status] of [
|
|||||||
name: "回复内容",
|
name: "回复内容",
|
||||||
exact: true,
|
exact: true,
|
||||||
});
|
});
|
||||||
assert.equal(await editor.getAttribute("contenteditable"), "false");
|
assert.equal(await editor.isEditable(), false);
|
||||||
assert.equal(await page.locator("#reply-files").isDisabled(), true);
|
assert.equal(await page.locator("#reply-files").isDisabled(), true);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
await page
|
await page
|
||||||
|
|||||||
Reference in new issue
Block a user