Files
gao-customer-h5/src/pages/management/RegistrationEventEdit.vue
T
2026-08-03 12:27:20 +08:00

608 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div v-can="pageAccess" class="registration-event-edit">
<t-form class="form" label-align="left">
<t-form-item label="名称">
<t-input v-model.trim="formModel.name" placeholder="登记事件名称" borderless />
</t-form-item>
<t-form-item label="说明">
<t-textarea v-model.trim="formModel.description" class="textarea" placeholder="事件说明" autosize borderless />
</t-form-item>
<t-form-item label="颜色">
<t-button
size="extra-small"
variant="outline"
class="color-trigger"
@click="openColorPicker"
>
<div class="content">
<span class="swatch" :style="{ background: formModel.color }" />
<span v-text="formModel.color" />
</div>
</t-button>
</t-form-item>
<t-form-item label="状态">
<t-radio-group v-model="formModel.status" class="inline-options" variant="default-filled">
<t-radio :block="false" borderless value="DRAFT">草稿</t-radio>
<t-radio :block="false" borderless value="ACTIVE">可登记</t-radio>
<t-radio :block="false" borderless value="INACTIVE">不可登记</t-radio>
</t-radio-group>
</t-form-item>
<t-form-item label="需要拍照" content-align="right">
<t-switch v-model="formModel.requirePhoto" />
</t-form-item>
<t-form-item label="排序">
<t-input v-model.trim="formModel.sortText" type="number" placeholder="排序值" borderless />
</t-form-item>
<t-form-item label="约束">
<t-radio-group v-model="formModel.limitType" class="limit-options" variant="default-filled">
<t-radio
v-for="option in limitOptions"
:key="option.value"
:block="false"
borderless
:value="option.value"
>
<span v-text="option.label" />
</t-radio>
</t-radio-group>
</t-form-item>
<t-form-item v-if="isValueLimitType(formModel.limitType)" label="数值">
<t-input v-model.trim="formModel.limitValueText" type="number" :placeholder="limitValuePlaceholder" borderless />
</t-form-item>
<t-form-item label="起始时间" arrow @click="openBeginAtPicker">
<div class="picker-text" v-text="formModel.beginAtText || '不限'" />
</t-form-item>
<t-form-item label="终止时间" arrow @click="openEndAtPicker">
<div class="picker-text" v-text="formModel.endAtText || '不限'" />
</t-form-item>
</t-form>
<section class="roles">
<div class="title">
<span>可登记角色</span>
<span class="hint">必须至少选择一个角色,上级角色拥有子级角色可登记事件的登记权限</span>
</div>
<t-checkbox-group v-model="formModel.roleCodeList" class="group">
<t-checkbox
v-for="item in roleOptions"
:key="item.code"
:value="item.code"
class="item"
borderless
>
<span v-text="item.name || item.code" />
</t-checkbox>
</t-checkbox-group>
</section>
<div class="bottom-actions">
<t-button
v-if="!isCreateMode"
v-can="deletePermissionCode"
class="delete bg-white"
theme="danger"
variant="outline"
:loading="removing"
:disabled="saving || removing"
@click="confirmRemoveRegistrationEvent"
>
删除事件
</t-button>
<t-button
v-can="savePermissionCode"
class="save"
theme="primary"
:loading="saving"
:disabled="removing"
@click="saveRegistrationEvent"
>
保存
</t-button>
</div>
</div>
<t-popup v-model="colorPickerVisible" placement="bottom">
<div class="color-popup">
<div class="header">
<t-button variant="text" size="small" @click="cancelColorPicker">取消</t-button>
<span class="title" v-text="'颜色选择'" />
<t-button theme="primary" variant="text" size="small" @click="confirmColorPicker">确定</t-button>
</div>
<t-color-picker
v-model="colorDraft"
format="HEX8"
type="multiple"
:swatch-colors="colorSwatchList"
/>
</div>
</t-popup>
<t-popup v-model="beginAtPickerVisible" placement="bottom">
<div class="picker-popup">
<div class="picker-header">
<t-button variant="text" size="small" @click="clearBeginAt">清空</t-button>
<span v-text="'起始时间'" />
<t-button variant="text" size="small" @click="beginAtPickerVisible = false">取消</t-button>
</div>
<t-date-time-picker
v-model="beginAtDraftText"
:default-value="beginAtDraftText || nowDateTimeText"
:mode="['date', 'minute']"
format="YYYY-MM-DD HH:mm"
title="起始时间"
@cancel="beginAtPickerVisible = false"
@confirm="confirmBeginAt"
/>
</div>
</t-popup>
<t-popup v-model="endAtPickerVisible" placement="bottom">
<div class="picker-popup">
<div class="picker-header">
<t-button variant="text" size="small" @click="clearEndAt">清空</t-button>
<span v-text="'终止时间'" />
<t-button variant="text" size="small" @click="endAtPickerVisible = false">取消</t-button>
</div>
<t-date-time-picker
v-model="endAtDraftText"
:default-value="endAtDraftText || nowDateTimeText"
:mode="['date', 'minute']"
format="YYYY-MM-DD HH:mm"
title="终止时间"
@cancel="endAtPickerVisible = false"
@confirm="confirmEndAt"
/>
</div>
</t-popup>
</template>
<script setup lang="ts">
import { Time } from "timi-web";
import { DialogPlugin, Toast } from "tdesign-mobile-vue";
import type { Role } from "timi-web";
import RegistrationEventAPI from "@/api/RegistrationEventAPI";
import GaoUserAPI from "@/api/GaoUserAPI";
import type { RegistrationEvent, RegistrationEventStatus, RegistrationLimitType } from "@/types/RegistrationEvent";
import { gaoPermissionCodes } from "@/utils/gaoPermission";
type LimitOption = {
label: string;
value: RegistrationLimitType;
};
defineOptions({
name: "RegistrationEventEdit"
});
const route = useRoute();
const router = useRouter();
const routeId = computed(() => String(route.params.id || ""));
const isCreateMode = computed(() => !routeId.value);
const savePermissionCode = computed(() => isCreateMode.value ? gaoPermissionCodes.registrationEvent.create : gaoPermissionCodes.registrationEvent.update);
const deletePermissionCode = gaoPermissionCodes.registrationEvent.delete;
const pageAccess = computed(() => ({
value: isCreateMode.value
? [gaoPermissionCodes.registrationEvent.read, gaoPermissionCodes.user.read, savePermissionCode.value]
: [gaoPermissionCodes.registrationEvent.read, gaoPermissionCodes.user.read, { value: [savePermissionCode.value, deletePermissionCode], mode: "or" as const }],
mode: "and" as const
}));
const roleOptions = ref<Role[]>([]);
const colorPickerVisible = ref(false);
const colorDraft = ref("#2f88ff");
const beginAtPickerVisible = ref(false);
const endAtPickerVisible = ref(false);
const beginAtDraftText = ref("");
const endAtDraftText = ref("");
const saving = ref(false);
const removing = ref(false);
const colorSwatchList = [
"#2f88ff",
"#0ea5e9",
"#10b981",
"#22c55e",
"#f59e0b",
"#f97316",
"#ef4444",
"#ec4899",
"#8b5cf6",
"#64748b"
];
const limitOptions: LimitOption[] = [
{ label: "不限", value: "NONE" },
{ label: "上午/下午各一次", value: "DAILY_HALF_DAY" },
{ label: "每天一次", value: "NATURAL_DAY" },
{ label: "间隔小时", value: "INTERVAL_HOUR" },
{ label: "每月一次", value: "NATURAL_MONTH" },
{ label: "事件总次数", value: "TOTAL_LIMIT" },
{ label: "单客户总次数", value: "CUSTOMER_TOTAL_LIMIT" }
];
const formModel = reactive({
id: "",
name: "",
description: "",
color: "#2f88ff",
status: "ACTIVE" as RegistrationEventStatus,
requirePhoto: false,
limitType: "NONE" as RegistrationLimitType,
limitValueText: "",
beginAtText: "",
endAtText: "",
sortText: "0",
roleCodeList: [] as string[]
});
const nowDateTimeText = computed(() => Time.formatDateTime(new Date()));
const limitValuePlaceholder = computed(() => {
if (formModel.limitType === "INTERVAL_HOUR") {
return "请输入间隔小时数";
}
if (formModel.limitType === "CUSTOMER_TOTAL_LIMIT") {
return "请输入单客户总次数上限";
}
return "请输入总次数上限";
});
function openColorPicker(): void {
colorDraft.value = formModel.color;
colorPickerVisible.value = true;
}
function cancelColorPicker(): void {
colorPickerVisible.value = false;
colorDraft.value = formModel.color;
}
function confirmColorPicker(): void {
formModel.color = colorDraft.value;
colorPickerVisible.value = false;
}
function isValueLimitType(value: RegistrationLimitType): boolean {
return value === "INTERVAL_HOUR" || value === "TOTAL_LIMIT" || value === "CUSTOMER_TOTAL_LIMIT";
}
function resolveLimitValue(): number | undefined {
if (!isValueLimitType(formModel.limitType)) {
return undefined;
}
const value = Number(formModel.limitValueText);
return 0 < value ? Math.floor(value) : undefined;
}
function resolveTimestamp(valueText: string): number | undefined {
if (!valueText.trim()) {
return undefined;
}
const date = new Date(valueText);
const timestamp = Time.toTimestamp(date);
return Number.isFinite(timestamp) ? timestamp : undefined;
}
function validateForm(): boolean {
if (!formModel.name.trim()) {
showToast("请输入登记事件名称", "warning");
return false;
}
if (!formModel.roleCodeList.length) {
showToast("请至少选择一个可登记角色", "warning");
return false;
}
if (isValueLimitType(formModel.limitType) && !resolveLimitValue()) {
showToast("请输入有效的约束数值", "warning");
return false;
}
const beginAt = resolveTimestamp(formModel.beginAtText);
const endAt = resolveTimestamp(formModel.endAtText);
if (beginAt !== undefined && endAt !== undefined && endAt < beginAt) {
showToast("终止时间不能早于起始时间", "warning");
return false;
}
return true;
}
function buildPayload(): RegistrationEvent {
return {
id: formModel.id || undefined,
name: formModel.name.trim(),
description: formModel.description,
color: formModel.color,
status: formModel.status,
requirePhoto: formModel.requirePhoto,
limitType: formModel.limitType,
limitValue: resolveLimitValue(),
beginAt: resolveTimestamp(formModel.beginAtText),
endAt: resolveTimestamp(formModel.endAtText),
sort: Math.floor(Number(formModel.sortText) || 0),
roleCodeList: [...formModel.roleCodeList]
};
}
function openBeginAtPicker(): void {
beginAtDraftText.value = formModel.beginAtText || nowDateTimeText.value;
beginAtPickerVisible.value = true;
}
function openEndAtPicker(): void {
endAtDraftText.value = formModel.endAtText || nowDateTimeText.value;
endAtPickerVisible.value = true;
}
function confirmBeginAt(): void {
formModel.beginAtText = beginAtDraftText.value;
beginAtPickerVisible.value = false;
}
function confirmEndAt(): void {
formModel.endAtText = endAtDraftText.value;
endAtPickerVisible.value = false;
}
function clearBeginAt(): void {
formModel.beginAtText = "";
beginAtDraftText.value = "";
beginAtPickerVisible.value = false;
}
function clearEndAt(): void {
formModel.endAtText = "";
endAtDraftText.value = "";
endAtPickerVisible.value = false;
}
async function saveRegistrationEvent(): Promise<void> {
if (!validateForm()) {
return;
}
saving.value = true;
try {
const payload = buildPayload();
if (isCreateMode.value) {
await RegistrationEventAPI.create(payload);
} else {
await RegistrationEventAPI.update(payload);
}
showToast("登记事件已保存");
await router.replace("/management/registration-events");
} catch {
showToast("登记事件保存失败", "error");
} finally {
saving.value = false;
}
}
async function confirmRemoveRegistrationEvent(): Promise<void> {
const id = formModel.id || routeId.value;
if (!id || removing.value) {
return;
}
const eventName = formModel.name.trim() || "未命名事件";
const dialog = DialogPlugin.confirm({
title: "删除登记事件",
content: `确定删除登记事件 ${eventName} 吗?删除后无法恢复。`,
cancelBtn: "取消",
confirmBtn: "删除",
onCancel() {
dialog.destroy();
},
async onConfirm() {
dialog.destroy();
await removeRegistrationEvent(id);
}
});
}
async function removeRegistrationEvent(id: string): Promise<void> {
if (removing.value) {
return;
}
removing.value = true;
try {
await RegistrationEventAPI.remove(id);
showToast("登记事件已删除");
await router.replace("/management/registration-events");
} catch {
showToast("登记事件删除失败", "error");
} finally {
removing.value = false;
}
}
function showToast(message: string, theme: "success" | "error" | "warning" = "success"): void {
Toast({
theme,
direction: "row",
message
});
}
async function fillForm(): Promise<void> {
roleOptions.value = await GaoUserAPI.roleList();
if (isCreateMode.value) {
return;
}
const current = await RegistrationEventAPI.detail(routeId.value);
Object.assign(formModel, {
id: current.id || "",
name: current.name || "",
description: current.description || "",
color: current.color || "#2f88ff",
status: current.status === "DRAFT" ? "DRAFT" : current.status === "INACTIVE" ? "INACTIVE" : "ACTIVE",
requirePhoto: current.requirePhoto ?? false,
limitType: current.limitType || "NONE",
limitValueText: current.limitValue ? String(current.limitValue) : "",
beginAtText: current.beginAt ? Time.formatDateTime(new Date(current.beginAt)) : "",
endAtText: current.endAt ? Time.formatDateTime(new Date(current.endAt)) : "",
sortText: String(current.sort ?? 0),
roleCodeList: [...(current.roleCodeList || [])]
});
colorDraft.value = formModel.color;
}
onMounted(async () => {
try {
await fillForm();
} catch {
showToast("登记事件加载失败", "error");
}
});
</script>
<style scoped lang="less">
.registration-event-edit {
padding: calc(var(--app-nav-height) + 1rem) 0 5rem 0;
.form {
border-radius: .75rem;
}
.picker-text {
color: #52607a;
width: 100%;
text-align: right;
}
.textarea {
width: 100%;
}
.color-trigger {
--td-button-extra-small-padding-horizontal: 2px;
gap: .75rem;
display: flex;
align-items: center;
padding-right: .875rem;
justify-content: flex-start;
.content {
gap: .5rem;
display: flex;
.swatch {
width: 1.5rem;
height: 1.5rem;
display: block;
border-radius: .25rem;
}
span {
color: #52607a;
font-size: .875rem;
}
}
}
.inline-options,
.limit-options {
gap: .5rem;
display: flex;
flex-wrap: wrap;
}
.roles {
gap: .75rem;
display: flex;
padding: 1rem;
flex-direction: column;
.title {
gap: .25rem;
display: flex;
flex-direction: column;
span:first-child {
font-weight: 700;
}
.hint {
color: #52607a;
font-size: .8125rem;
}
}
.group {
gap: .5rem;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
.item {
margin: 0;
padding: .875rem;
background: #fff;
border: 1px solid rgba(148, 163, 184, .22);
border-radius: .75rem;
}
}
}
.bottom-actions {
gap: .75rem;
left: 1rem;
right: 1rem;
display: flex;
bottom: calc(.75rem + env(safe-area-inset-bottom, 0));
position: fixed;
.delete {
flex: 1;
}
.save {
flex: 2;
}
}
}
.picker-popup {
background: var(--td-bg-color-container);
.picker-header {
display: flex;
padding: .75rem 1rem 0;
align-items: center;
justify-content: space-between;
}
}
.color-popup {
padding: .75rem 0 1.25rem;
background: #fff;
border-radius: 1rem 1rem 0 0;
.header {
display: flex;
padding: 0 1rem;
align-items: center;
justify-content: space-between;
span {
font-size: 1rem;
font-weight: 700;
}
}
}
:global(html.theme-dark) {
.registration-event-edit {
.color-trigger span,
.roles .title .hint {
color: #94a3b8;
}
.color-trigger .swatch {
box-shadow: inset 0 0 0 1px rgba(148, 163, 184, .24);
}
.color-popup {
background: #0f172a;
}
.roles .group .item {
color: #e2e8f0;
background: rgba(15, 23, 42, .72);
border-color: rgba(148, 163, 184, .16);
}
}
}
</style>