608 lines
16 KiB
Vue
608 lines
16 KiB
Vue
<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>
|