update registration event editor
This commit is contained in:
@@ -184,7 +184,7 @@ onUnmounted(() => {
|
|||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
.customer-selector {
|
.customer-selector {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 70vh;
|
height: 80vh;
|
||||||
padding-top: 1rem;
|
padding-top: 1rem;
|
||||||
border-radius: 1.25rem 1.25rem 0 0;
|
border-radius: 1.25rem 1.25rem 0 0;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
theme="primary"
|
theme="primary"
|
||||||
variant="dashed"
|
variant="dashed"
|
||||||
class="add"
|
class="add"
|
||||||
|
:loading="loading"
|
||||||
|
:disabled="loading"
|
||||||
@click="$emit('add')"
|
@click="$emit('add')"
|
||||||
>
|
>
|
||||||
<span v-text="addText" />
|
<span v-text="addText" />
|
||||||
@@ -18,8 +20,11 @@
|
|||||||
fit="cover"
|
fit="cover"
|
||||||
/>
|
/>
|
||||||
</t-button>
|
</t-button>
|
||||||
|
<div v-if="loading" class="mask">
|
||||||
|
<t-loading size="1.5rem" text="上传中" />
|
||||||
|
</div>
|
||||||
<t-button
|
<t-button
|
||||||
v-if="showRemove"
|
v-if="showRemove && !loading"
|
||||||
class="remove"
|
class="remove"
|
||||||
theme="danger"
|
theme="danger"
|
||||||
shape="circle"
|
shape="circle"
|
||||||
@@ -42,6 +47,7 @@ interface Props {
|
|||||||
addText?: string;
|
addText?: string;
|
||||||
altText?: string;
|
altText?: string;
|
||||||
modelValue: string;
|
modelValue: string;
|
||||||
|
loading?: boolean;
|
||||||
preview?: boolean;
|
preview?: boolean;
|
||||||
showRemove?: boolean;
|
showRemove?: boolean;
|
||||||
size?: "normal" | "compact";
|
size?: "normal" | "compact";
|
||||||
@@ -50,6 +56,7 @@ interface Props {
|
|||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
addText: "拍照",
|
addText: "拍照",
|
||||||
altText: "照片",
|
altText: "照片",
|
||||||
|
loading: false,
|
||||||
preview: true,
|
preview: true,
|
||||||
showRemove: true,
|
showRemove: true,
|
||||||
size: "normal"
|
size: "normal"
|
||||||
@@ -133,6 +140,19 @@ function clearPhoto(): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mask {
|
||||||
|
inset: 0;
|
||||||
|
gap: .375rem;
|
||||||
|
z-index: 1;
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
position: absolute;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(15, 23, 42, .45);
|
||||||
|
}
|
||||||
|
|
||||||
&.compact {
|
&.compact {
|
||||||
.add,
|
.add,
|
||||||
.card {
|
.card {
|
||||||
|
|||||||
@@ -29,6 +29,8 @@
|
|||||||
theme="primary"
|
theme="primary"
|
||||||
variant="dashed"
|
variant="dashed"
|
||||||
class="add"
|
class="add"
|
||||||
|
:loading="loading"
|
||||||
|
:disabled="loading"
|
||||||
@click="$emit('add')"
|
@click="$emit('add')"
|
||||||
>
|
>
|
||||||
<span v-text="addText" />
|
<span v-text="addText" />
|
||||||
@@ -47,6 +49,7 @@ interface Props {
|
|||||||
addText?: string;
|
addText?: string;
|
||||||
emptyText?: string;
|
emptyText?: string;
|
||||||
fallbackImage?: string;
|
fallbackImage?: string;
|
||||||
|
loading?: boolean;
|
||||||
modelValue: string[];
|
modelValue: string[];
|
||||||
showAdd?: boolean;
|
showAdd?: boolean;
|
||||||
showRemove?: boolean;
|
showRemove?: boolean;
|
||||||
@@ -57,6 +60,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
title: "照片列表",
|
title: "照片列表",
|
||||||
addText: "拍照",
|
addText: "拍照",
|
||||||
emptyText: "暂无照片",
|
emptyText: "暂无照片",
|
||||||
|
loading: false,
|
||||||
showAdd: true,
|
showAdd: true,
|
||||||
showRemove: true
|
showRemove: true
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -52,6 +52,7 @@
|
|||||||
<photo-field
|
<photo-field
|
||||||
v-model="supplementPhoto"
|
v-model="supplementPhoto"
|
||||||
alt-text="客户照片"
|
alt-text="客户照片"
|
||||||
|
:loading="supplementPhotoUploading"
|
||||||
:preview="false"
|
:preview="false"
|
||||||
@add="selectSupplementPhoto"
|
@add="selectSupplementPhoto"
|
||||||
@clear="clearSupplementPhoto"
|
@clear="clearSupplementPhoto"
|
||||||
@@ -76,6 +77,7 @@
|
|||||||
<photo-scroller
|
<photo-scroller
|
||||||
title="登记事件照片"
|
title="登记事件照片"
|
||||||
v-model="photoList"
|
v-model="photoList"
|
||||||
|
:loading="recordPhotoUploading"
|
||||||
@add="selectRecordPhoto"
|
@add="selectRecordPhoto"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -133,6 +135,8 @@ const supplementPhotoTempFileId = ref("");
|
|||||||
const supplementPhotoInputRef = ref<HTMLInputElement>();
|
const supplementPhotoInputRef = ref<HTMLInputElement>();
|
||||||
const recordPhotoInputRef = ref<HTMLInputElement>();
|
const recordPhotoInputRef = ref<HTMLInputElement>();
|
||||||
const recordPhotoTempFileIdList = ref<string[]>([]);
|
const recordPhotoTempFileIdList = ref<string[]>([]);
|
||||||
|
const supplementPhotoUploading = ref(false);
|
||||||
|
const recordPhotoUploading = ref(false);
|
||||||
const registrationEventList = ref<RegistrationEvent[]>([]);
|
const registrationEventList = ref<RegistrationEvent[]>([]);
|
||||||
const selectedRegistrationEventIdList = ref<string[]>([]);
|
const selectedRegistrationEventIdList = ref<string[]>([]);
|
||||||
const note = ref("");
|
const note = ref("");
|
||||||
@@ -375,6 +379,8 @@ async function handleSupplementPhotoSelected(event: Event): Promise<void> {
|
|||||||
if (!file) {
|
if (!file) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
supplementPhotoUploading.value = true;
|
||||||
|
try {
|
||||||
const compressedFile = await compressUploadImage(file);
|
const compressedFile = await compressUploadImage(file);
|
||||||
const [tempFile] = await AttachmentAPI.uploadTemp(compressedFile);
|
const [tempFile] = await AttachmentAPI.uploadTemp(compressedFile);
|
||||||
if (!tempFile?.id) {
|
if (!tempFile?.id) {
|
||||||
@@ -383,8 +389,11 @@ async function handleSupplementPhotoSelected(event: Event): Promise<void> {
|
|||||||
}
|
}
|
||||||
supplementPhotoTempFileId.value = tempFile.id;
|
supplementPhotoTempFileId.value = tempFile.id;
|
||||||
supplementPhoto.value = AttachmentAPI.getTempReadURL(tempFile.id);
|
supplementPhoto.value = AttachmentAPI.getTempReadURL(tempFile.id);
|
||||||
|
} finally {
|
||||||
|
supplementPhotoUploading.value = false;
|
||||||
input.value = "";
|
input.value = "";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function selectRecordPhoto(): void {
|
function selectRecordPhoto(): void {
|
||||||
recordPhotoInputRef.value?.click();
|
recordPhotoInputRef.value?.click();
|
||||||
@@ -396,6 +405,8 @@ async function handleRecordPhotoSelected(event: Event): Promise<void> {
|
|||||||
if (!fileList.length) {
|
if (!fileList.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
recordPhotoUploading.value = true;
|
||||||
|
try {
|
||||||
const compressedFileList = await compressUploadImageList(fileList);
|
const compressedFileList = await compressUploadImageList(fileList);
|
||||||
const tempFileList = await AttachmentAPI.uploadTemp(compressedFileList);
|
const tempFileList = await AttachmentAPI.uploadTemp(compressedFileList);
|
||||||
for (const item of tempFileList) {
|
for (const item of tempFileList) {
|
||||||
@@ -405,8 +416,11 @@ async function handleRecordPhotoSelected(event: Event): Promise<void> {
|
|||||||
recordPhotoTempFileIdList.value.push(item.id);
|
recordPhotoTempFileIdList.value.push(item.id);
|
||||||
photoList.value.push(AttachmentAPI.getTempReadURL(item.id));
|
photoList.value.push(AttachmentAPI.getTempReadURL(item.id));
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
recordPhotoUploading.value = false;
|
||||||
input.value = "";
|
input.value = "";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- 提交 ----------
|
// ---------- 提交 ----------
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,7 @@
|
|||||||
<photo-field
|
<photo-field
|
||||||
v-model="formModel.photoURL"
|
v-model="formModel.photoURL"
|
||||||
alt-text="客户照片"
|
alt-text="客户照片"
|
||||||
|
:loading="photoUploading"
|
||||||
:preview="false"
|
:preview="false"
|
||||||
@add="selectPhoto"
|
@add="selectPhoto"
|
||||||
@clear="resetPhotoState"
|
@clear="resetPhotoState"
|
||||||
@@ -177,6 +178,7 @@ const selectorVisible = ref(false);
|
|||||||
const referrerCustomerId = ref("");
|
const referrerCustomerId = ref("");
|
||||||
const photoInputRef = ref<HTMLInputElement>();
|
const photoInputRef = ref<HTMLInputElement>();
|
||||||
const photoTempFileId = ref("");
|
const photoTempFileId = ref("");
|
||||||
|
const photoUploading = ref(false);
|
||||||
const scannerVisible = ref(false);
|
const scannerVisible = ref(false);
|
||||||
const scannerTarget = ref<ScanTarget>("code");
|
const scannerTarget = ref<ScanTarget>("code");
|
||||||
const scanStatus = ref("点击扫码开始识别。");
|
const scanStatus = ref("点击扫码开始识别。");
|
||||||
@@ -321,6 +323,8 @@ async function handlePhotoSelected(event: Event): Promise<void> {
|
|||||||
if (!file) {
|
if (!file) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
photoUploading.value = true;
|
||||||
|
try {
|
||||||
const compressedFile = await compressUploadImage(file);
|
const compressedFile = await compressUploadImage(file);
|
||||||
const [tempFile] = await AttachmentAPI.uploadTemp(compressedFile);
|
const [tempFile] = await AttachmentAPI.uploadTemp(compressedFile);
|
||||||
if (!tempFile?.id) {
|
if (!tempFile?.id) {
|
||||||
@@ -329,8 +333,11 @@ async function handlePhotoSelected(event: Event): Promise<void> {
|
|||||||
}
|
}
|
||||||
photoTempFileId.value = tempFile.id;
|
photoTempFileId.value = tempFile.id;
|
||||||
formModel.photoURL = AttachmentAPI.getTempReadURL(tempFile.id);
|
formModel.photoURL = AttachmentAPI.getTempReadURL(tempFile.id);
|
||||||
|
} finally {
|
||||||
|
photoUploading.value = false;
|
||||||
input.value = "";
|
input.value = "";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- 介绍人 ----------
|
// ---------- 介绍人 ----------
|
||||||
|
|
||||||
|
|||||||
@@ -78,6 +78,11 @@ const monthRecords = ref<RegistrationEventRecord[]>([]);
|
|||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const calendarLoading = ref(false);
|
const calendarLoading = ref(false);
|
||||||
const monthCursor = ref(getMonthStart(new Date()));
|
const monthCursor = ref(getMonthStart(new Date()));
|
||||||
|
const genderColorMap = {
|
||||||
|
MALE: "#2563eb",
|
||||||
|
FEMALE: "#db2777",
|
||||||
|
UNKNOWN: "#64748b"
|
||||||
|
} as const;
|
||||||
const beginDate = getDateStart(addDays(new Date(), -6));
|
const beginDate = getDateStart(addDays(new Date(), -6));
|
||||||
const endDate = getDateEnd(new Date());
|
const endDate = getDateEnd(new Date());
|
||||||
const dateTextList = buildDateTextList(beginDate, endDate);
|
const dateTextList = buildDateTextList(beginDate, endDate);
|
||||||
@@ -305,6 +310,9 @@ function buildRecordTrendOption(): EChartsOption {
|
|||||||
function buildEventStructureOption(): EChartsOption {
|
function buildEventStructureOption(): EChartsOption {
|
||||||
return {
|
return {
|
||||||
...buildBaseOption(),
|
...buildBaseOption(),
|
||||||
|
legend: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
xAxis: {
|
xAxis: {
|
||||||
type: "category",
|
type: "category",
|
||||||
data: registrationEventList.value.map(item => item.name || "未命名事件"),
|
data: registrationEventList.value.map(item => item.name || "未命名事件"),
|
||||||
@@ -345,9 +353,24 @@ function buildGenderStatOption(): EChartsOption {
|
|||||||
name: "客户数量",
|
name: "客户数量",
|
||||||
type: "bar",
|
type: "bar",
|
||||||
data: [
|
data: [
|
||||||
getGenderCount("MALE"),
|
{
|
||||||
getGenderCount("FEMALE"),
|
value: getGenderCount("MALE"),
|
||||||
getGenderCount("UNKNOWN")
|
itemStyle: {
|
||||||
|
color: genderColorMap.MALE
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: getGenderCount("FEMALE"),
|
||||||
|
itemStyle: {
|
||||||
|
color: genderColorMap.FEMALE
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: getGenderCount("UNKNOWN"),
|
||||||
|
itemStyle: {
|
||||||
|
color: genderColorMap.UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}]
|
}]
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
add-text="更换"
|
add-text="更换"
|
||||||
alt-text="用户头像"
|
alt-text="用户头像"
|
||||||
size="compact"
|
size="compact"
|
||||||
|
:loading="avatarUploading"
|
||||||
:preview="false"
|
:preview="false"
|
||||||
:show-remove="false"
|
:show-remove="false"
|
||||||
@add="changeAvatar"
|
@add="changeAvatar"
|
||||||
@@ -63,6 +64,7 @@ const router = useRouter();
|
|||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const avatarInputRef = ref<HTMLInputElement>();
|
const avatarInputRef = ref<HTMLInputElement>();
|
||||||
const avatarTempFileId = ref("");
|
const avatarTempFileId = ref("");
|
||||||
|
const avatarUploading = ref(false);
|
||||||
const isSaving = ref(false);
|
const isSaving = ref(false);
|
||||||
|
|
||||||
const formModel = reactive({
|
const formModel = reactive({
|
||||||
@@ -108,6 +110,8 @@ async function handleAvatarSelected(event: Event): Promise<void> {
|
|||||||
if (!file) {
|
if (!file) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
avatarUploading.value = true;
|
||||||
|
try {
|
||||||
const compressedFile = await compressUploadImage(file);
|
const compressedFile = await compressUploadImage(file);
|
||||||
const [tempFile] = await AttachmentAPI.uploadTemp(compressedFile);
|
const [tempFile] = await AttachmentAPI.uploadTemp(compressedFile);
|
||||||
if (!tempFile?.id) {
|
if (!tempFile?.id) {
|
||||||
@@ -120,8 +124,11 @@ async function handleAvatarSelected(event: Event): Promise<void> {
|
|||||||
}
|
}
|
||||||
avatarTempFileId.value = tempFile.id;
|
avatarTempFileId.value = tempFile.id;
|
||||||
formModel.avatar = AttachmentAPI.getTempReadURL(tempFile.id);
|
formModel.avatar = AttachmentAPI.getTempReadURL(tempFile.id);
|
||||||
|
} finally {
|
||||||
|
avatarUploading.value = false;
|
||||||
input.value = "";
|
input.value = "";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function goPassword(): Promise<void> {
|
async function goPassword(): Promise<void> {
|
||||||
await router.push("/management/profile/password");
|
await router.push("/management/profile/password");
|
||||||
|
|||||||
@@ -8,10 +8,17 @@
|
|||||||
<t-textarea v-model.trim="formModel.description" class="textarea" placeholder="事件说明" autosize borderless />
|
<t-textarea v-model.trim="formModel.description" class="textarea" placeholder="事件说明" autosize borderless />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label="颜色">
|
<t-form-item label="颜色">
|
||||||
<label class="color-picker">
|
<t-button
|
||||||
<input v-model="formModel.color" type="color">
|
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" />
|
<span v-text="formModel.color" />
|
||||||
</label>
|
</div>
|
||||||
|
</t-button>
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label="状态">
|
<t-form-item label="状态">
|
||||||
<t-radio-group v-model="formModel.status" class="inline-options" variant="default-filled">
|
<t-radio-group v-model="formModel.status" class="inline-options" variant="default-filled">
|
||||||
@@ -19,10 +26,10 @@
|
|||||||
<t-radio :block="false" borderless value="INACTIVE">无效</t-radio>
|
<t-radio :block="false" borderless value="INACTIVE">无效</t-radio>
|
||||||
</t-radio-group>
|
</t-radio-group>
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label="启用">
|
<t-form-item label="启用" content-align="right">
|
||||||
<t-switch v-model="formModel.enabled" />
|
<t-switch v-model="formModel.enabled" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label="拍照">
|
<t-form-item label="需要拍照" content-align="right">
|
||||||
<t-switch v-model="formModel.requirePhoto" />
|
<t-switch v-model="formModel.requirePhoto" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label="排序">
|
<t-form-item label="排序">
|
||||||
@@ -62,12 +69,50 @@
|
|||||||
</t-checkbox>
|
</t-checkbox>
|
||||||
</t-checkbox-group>
|
</t-checkbox-group>
|
||||||
</section>
|
</section>
|
||||||
<t-button v-can="savePermissionCode" class="save" theme="primary" @click="saveRegistrationEvent">保存</t-button>
|
<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>
|
||||||
|
</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>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Toast } from "tdesign-mobile-vue";
|
import { DialogPlugin, Toast } from "tdesign-mobile-vue";
|
||||||
import type { Role } from "timi-web";
|
import type { Role } from "timi-web";
|
||||||
import RegistrationEventAPI from "@/api/RegistrationEventAPI";
|
import RegistrationEventAPI from "@/api/RegistrationEventAPI";
|
||||||
import GaoUserAPI from "@/api/GaoUserAPI";
|
import GaoUserAPI from "@/api/GaoUserAPI";
|
||||||
@@ -88,11 +133,30 @@ const router = useRouter();
|
|||||||
const routeId = computed(() => String(route.params.id || ""));
|
const routeId = computed(() => String(route.params.id || ""));
|
||||||
const isCreateMode = computed(() => !routeId.value);
|
const isCreateMode = computed(() => !routeId.value);
|
||||||
const savePermissionCode = computed(() => isCreateMode.value ? gaoPermissionCodes.registrationEvent.create : gaoPermissionCodes.registrationEvent.update);
|
const savePermissionCode = computed(() => isCreateMode.value ? gaoPermissionCodes.registrationEvent.create : gaoPermissionCodes.registrationEvent.update);
|
||||||
|
const deletePermissionCode = gaoPermissionCodes.registrationEvent.delete;
|
||||||
const pageAccess = computed(() => ({
|
const pageAccess = computed(() => ({
|
||||||
value: [gaoPermissionCodes.registrationEvent.read, gaoPermissionCodes.user.read, savePermissionCode.value],
|
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
|
mode: "and" as const
|
||||||
}));
|
}));
|
||||||
const roleOptions = ref<Role[]>([]);
|
const roleOptions = ref<Role[]>([]);
|
||||||
|
const colorPickerVisible = ref(false);
|
||||||
|
const colorDraft = ref("#2f88ff");
|
||||||
|
const saving = ref(false);
|
||||||
|
const removing = ref(false);
|
||||||
|
const colorSwatchList = [
|
||||||
|
"#2f88ff",
|
||||||
|
"#0ea5e9",
|
||||||
|
"#10b981",
|
||||||
|
"#22c55e",
|
||||||
|
"#f59e0b",
|
||||||
|
"#f97316",
|
||||||
|
"#ef4444",
|
||||||
|
"#ec4899",
|
||||||
|
"#8b5cf6",
|
||||||
|
"#64748b"
|
||||||
|
];
|
||||||
const limitOptions: LimitOption[] = [
|
const limitOptions: LimitOption[] = [
|
||||||
{ label: "不限", value: "NONE" },
|
{ label: "不限", value: "NONE" },
|
||||||
{ label: "上午/下午各一次", value: "DAILY_HALF_DAY" },
|
{ label: "上午/下午各一次", value: "DAILY_HALF_DAY" },
|
||||||
@@ -116,6 +180,21 @@ const formModel = reactive({
|
|||||||
});
|
});
|
||||||
const limitValuePlaceholder = computed(() => formModel.limitType === "INTERVAL_HOUR" ? "请输入间隔小时数" : "请输入累计上限次数");
|
const limitValuePlaceholder = computed(() => formModel.limitType === "INTERVAL_HOUR" ? "请输入间隔小时数" : "请输入累计上限次数");
|
||||||
|
|
||||||
|
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 {
|
function isValueLimitType(value: RegistrationLimitType): boolean {
|
||||||
return value === "INTERVAL_HOUR" || value === "TOTAL_LIMIT";
|
return value === "INTERVAL_HOUR" || value === "TOTAL_LIMIT";
|
||||||
}
|
}
|
||||||
@@ -162,6 +241,7 @@ async function saveRegistrationEvent(): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
saving.value = true;
|
||||||
try {
|
try {
|
||||||
const payload = buildPayload();
|
const payload = buildPayload();
|
||||||
if (isCreateMode.value) {
|
if (isCreateMode.value) {
|
||||||
@@ -173,6 +253,47 @@ async function saveRegistrationEvent(): Promise<void> {
|
|||||||
await router.replace("/management/registration-events");
|
await router.replace("/management/registration-events");
|
||||||
} catch {
|
} catch {
|
||||||
showToast("登记事件保存失败", "error");
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,6 +325,7 @@ async function fillForm(): Promise<void> {
|
|||||||
sortText: String(current.sort ?? 0),
|
sortText: String(current.sort ?? 0),
|
||||||
roleCodeList: [...(current.roleCodeList || [])]
|
roleCodeList: [...(current.roleCodeList || [])]
|
||||||
});
|
});
|
||||||
|
colorDraft.value = formModel.color;
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@@ -227,17 +349,24 @@ onMounted(async () => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.color-picker {
|
.color-trigger {
|
||||||
|
--td-button-extra-small-padding-horizontal: 2px;
|
||||||
|
|
||||||
gap: .75rem;
|
gap: .75rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
padding-right: .875rem;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
input {
|
.content {
|
||||||
padding: 0;
|
gap: .5rem;
|
||||||
width: 2rem;
|
display: flex;
|
||||||
height: 2rem;
|
|
||||||
border: 0;
|
.swatch {
|
||||||
background: transparent;
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
display: block;
|
||||||
|
border-radius: .25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
span {
|
span {
|
||||||
@@ -245,6 +374,7 @@ onMounted(async () => {
|
|||||||
font-size: .875rem;
|
font-size: .875rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.inline-options,
|
.inline-options,
|
||||||
.limit-options {
|
.limit-options {
|
||||||
@@ -289,21 +419,57 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.save {
|
.bottom-actions {
|
||||||
|
gap: .75rem;
|
||||||
left: 1rem;
|
left: 1rem;
|
||||||
right: 1rem;
|
right: 1rem;
|
||||||
|
display: flex;
|
||||||
bottom: calc(.75rem + env(safe-area-inset-bottom, 0));
|
bottom: calc(.75rem + env(safe-area-inset-bottom, 0));
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|
||||||
|
.delete {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save {
|
||||||
|
flex: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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) {
|
:global(html.theme-dark) {
|
||||||
.registration-event-edit {
|
.registration-event-edit {
|
||||||
.color-picker span,
|
.color-trigger span,
|
||||||
.roles .title .hint {
|
.roles .title .hint {
|
||||||
color: #94a3b8;
|
color: #94a3b8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.color-trigger .swatch {
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(148, 163, 184, .24);
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-popup {
|
||||||
|
background: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
.roles .group .item {
|
.roles .group .item {
|
||||||
color: #e2e8f0;
|
color: #e2e8f0;
|
||||||
background: rgba(15, 23, 42, .72);
|
background: rgba(15, 23, 42, .72);
|
||||||
|
|||||||
@@ -1,31 +1,31 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-can="{ value: [gaoPermissionCodes.registrationEvent.read, gaoPermissionCodes.user.read], mode: 'and' }" class="registration-event-list">
|
<div v-can="{ value: [gaoPermissionCodes.registrationEvent.read, gaoPermissionCodes.user.read], mode: 'and' }" class="registration-event-list">
|
||||||
<article
|
<t-cell-group theme="card">
|
||||||
|
<t-cell
|
||||||
class="item"
|
class="item"
|
||||||
v-for="item in registrationEventList"
|
v-for="item in registrationEventList"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
|
:arrow="canUpdateRegistrationEvent"
|
||||||
|
:hover="canUpdateRegistrationEvent"
|
||||||
|
@click="handleRegistrationEventClick(item)"
|
||||||
>
|
>
|
||||||
<div class="main">
|
<template #title>
|
||||||
<span class="color" :style="{ background: item.color || defaultColor }" />
|
<div class="header">
|
||||||
<div class="content">
|
<div class="color" :style="{ background: item.color || defaultColor }" />
|
||||||
<div class="title">
|
<div class="title" v-text="item.name || '未命名事件'" />
|
||||||
<strong v-text="item.name || '未命名事件'" />
|
|
||||||
<t-tag
|
<t-tag
|
||||||
size="small"
|
class="status"
|
||||||
theme="primary"
|
:theme="isRegistrationEventActive(item) ? 'primary' : 'default'"
|
||||||
:variant="isRegistrationEventActive(item) ? 'light' : 'outline'"
|
variant="outline"
|
||||||
:content="getStatusText(item)"
|
:content="getStatusText(item)"
|
||||||
/>
|
/>
|
||||||
|
<t-tag v-if="item.requirePhoto" theme="warning" variant="outline">需拍照</t-tag>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="item.description" v-text="item.description" />
|
</template>
|
||||||
<div class="meta">
|
<template #description>
|
||||||
<span v-text="getLimitText(item)" />
|
<div class="description">
|
||||||
<span v-if="item.requirePhoto">需拍照</span>
|
<div class="line">
|
||||||
<span v-text="`${item.roleCodeList?.length || 0} 个角色`" />
|
<span>可登记角色</span>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="roles">
|
|
||||||
<t-tag
|
<t-tag
|
||||||
v-for="roleCode in item.roleCodeList"
|
v-for="roleCode in item.roleCodeList"
|
||||||
:key="roleCode"
|
:key="roleCode"
|
||||||
@@ -34,33 +34,11 @@
|
|||||||
:content="getRoleName(roleCode)"
|
:content="getRoleName(roleCode)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="line" v-text="getLimitText(item)" />
|
||||||
<t-button
|
|
||||||
v-can="gaoPermissionCodes.registrationEvent.update"
|
|
||||||
theme="primary"
|
|
||||||
variant="outline"
|
|
||||||
size="small"
|
|
||||||
@click="editRegistrationEvent(item)"
|
|
||||||
content="编辑"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<t-icon name="edit-1" size="small" />
|
|
||||||
</template>
|
|
||||||
</t-button>
|
|
||||||
<t-button
|
|
||||||
v-can="gaoPermissionCodes.registrationEvent.delete"
|
|
||||||
theme="danger"
|
|
||||||
variant="outline"
|
|
||||||
size="small"
|
|
||||||
@click="removeRegistrationEvent(item)"
|
|
||||||
content="删除"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<t-icon name="delete" size="small" />
|
|
||||||
</template>
|
|
||||||
</t-button>
|
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</template>
|
||||||
|
</t-cell>
|
||||||
|
</t-cell-group>
|
||||||
<div v-if="!registrationEventList.length" class="empty-block">暂无登记事件</div>
|
<div v-if="!registrationEventList.length" class="empty-block">暂无登记事件</div>
|
||||||
<t-button v-can="gaoPermissionCodes.registrationEvent.create" class="create" theme="primary" @click="createRegistrationEvent">
|
<t-button v-can="gaoPermissionCodes.registrationEvent.create" class="create" theme="primary" @click="createRegistrationEvent">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
@@ -73,7 +51,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Toast } from "tdesign-mobile-vue";
|
import { Toast } from "tdesign-mobile-vue";
|
||||||
import type { Role } from "timi-web";
|
import { useUserStore, type Role } from "timi-web";
|
||||||
import RegistrationEventAPI from "@/api/RegistrationEventAPI";
|
import RegistrationEventAPI from "@/api/RegistrationEventAPI";
|
||||||
import GaoUserAPI from "@/api/GaoUserAPI";
|
import GaoUserAPI from "@/api/GaoUserAPI";
|
||||||
import type { RegistrationEvent, RegistrationLimitType } from "@/types/RegistrationEvent";
|
import type { RegistrationEvent, RegistrationLimitType } from "@/types/RegistrationEvent";
|
||||||
@@ -84,9 +62,11 @@ defineOptions({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const userStore = useUserStore();
|
||||||
const defaultColor = "#2f88ff";
|
const defaultColor = "#2f88ff";
|
||||||
const registrationEventList = ref<RegistrationEvent[]>([]);
|
const registrationEventList = ref<RegistrationEvent[]>([]);
|
||||||
const roleOptions = ref<Role[]>([]);
|
const roleOptions = ref<Role[]>([]);
|
||||||
|
const canUpdateRegistrationEvent = computed(() => userStore.hasPermissions(gaoPermissionCodes.registrationEvent.update));
|
||||||
|
|
||||||
function createRegistrationEvent(): void {
|
function createRegistrationEvent(): void {
|
||||||
router.push("/management/registration-events/create");
|
router.push("/management/registration-events/create");
|
||||||
@@ -100,26 +80,12 @@ function editRegistrationEvent(item: RegistrationEvent): void {
|
|||||||
router.push(`/management/registration-events/${item.id}/edit`);
|
router.push(`/management/registration-events/${item.id}/edit`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeRegistrationEvent(item: RegistrationEvent): Promise<void> {
|
function handleRegistrationEventClick(item: RegistrationEvent): void {
|
||||||
if (!item.id) {
|
if (!canUpdateRegistrationEvent.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
editRegistrationEvent(item);
|
||||||
await RegistrationEventAPI.remove(item.id);
|
|
||||||
Toast({
|
|
||||||
theme: "success",
|
|
||||||
direction: "row",
|
|
||||||
message: "登记事件已删除"
|
|
||||||
});
|
|
||||||
await loadData();
|
|
||||||
} catch {
|
|
||||||
Toast({
|
|
||||||
theme: "error",
|
|
||||||
direction: "row",
|
|
||||||
message: "登记事件删除失败"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isRegistrationEventActive(item: RegistrationEvent): boolean {
|
function isRegistrationEventActive(item: RegistrationEvent): boolean {
|
||||||
@@ -180,83 +146,31 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
.registration-event-list {
|
.registration-event-list {
|
||||||
gap: .875rem;
|
padding: calc(var(--app-nav-height) + 1rem) 0 6rem 0;
|
||||||
padding: calc(var(--app-nav-height) + 1rem) 1rem 6rem 1rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
gap: .75rem;
|
|
||||||
padding: 1rem;
|
|
||||||
display: grid;
|
|
||||||
border: 1px solid rgba(148, 163, 184, .16);
|
|
||||||
background: rgba(255, 255, 255, .96);
|
|
||||||
box-shadow: var(--td-shadow-1);
|
|
||||||
border-radius: .75rem;
|
|
||||||
|
|
||||||
.main {
|
.header {
|
||||||
gap: .75rem;
|
gap: .5rem;
|
||||||
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
min-width: 0;
|
margin-top: .25rem;
|
||||||
align-items: flex-start;
|
|
||||||
|
|
||||||
.color {
|
.color {
|
||||||
width: .875rem;
|
width: 1.5rem;
|
||||||
height: .875rem;
|
height: 1.5rem;
|
||||||
flex: 0 0 auto;
|
border-radius: .4rem;
|
||||||
margin-top: .25rem;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
|
|
||||||
.title {
|
|
||||||
gap: .5rem;
|
|
||||||
display: flex;
|
|
||||||
min-width: 0;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
strong {
|
|
||||||
overflow: hidden;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 700;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
.description {
|
||||||
margin: .375rem 0 0 0;
|
|
||||||
color: #52607a;
|
|
||||||
font-size: .875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta {
|
|
||||||
gap: .5rem;
|
|
||||||
display: flex;
|
|
||||||
margin-top: .5rem;
|
margin-top: .5rem;
|
||||||
flex-wrap: wrap;
|
|
||||||
|
|
||||||
span {
|
.line {
|
||||||
color: #52607a;
|
|
||||||
font-size: .75rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.roles {
|
|
||||||
gap: .375rem;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
gap: .5rem;
|
gap: .5rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
align-items: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,7 @@
|
|||||||
</t-form-item>
|
</t-form-item>
|
||||||
|
|
||||||
<div class="photo-part">
|
<div class="photo-part">
|
||||||
<photo-scroller v-model="photoList" @add="capturePhoto" />
|
<photo-scroller v-model="photoList" :loading="photoUploading" @add="capturePhoto" />
|
||||||
</div>
|
</div>
|
||||||
</t-form>
|
</t-form>
|
||||||
<t-cell-group
|
<t-cell-group
|
||||||
@@ -161,6 +161,7 @@ const registeredAtPickerVisible = ref(false);
|
|||||||
const note = ref("");
|
const note = ref("");
|
||||||
const recordPhotoInputRef = ref<HTMLInputElement>();
|
const recordPhotoInputRef = ref<HTMLInputElement>();
|
||||||
const photoItemList = ref<EditablePhoto[]>([]);
|
const photoItemList = ref<EditablePhoto[]>([]);
|
||||||
|
const photoUploading = ref(false);
|
||||||
const scannerVisible = ref(false);
|
const scannerVisible = ref(false);
|
||||||
const scanStatus = ref("点击扫码开始识别。");
|
const scanStatus = ref("点击扫码开始识别。");
|
||||||
const saving = ref(false);
|
const saving = ref(false);
|
||||||
@@ -366,6 +367,8 @@ async function handleRecordPhotoSelected(event: Event): Promise<void> {
|
|||||||
if (!fileList.length) {
|
if (!fileList.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
photoUploading.value = true;
|
||||||
|
try {
|
||||||
const compressedFileList = await compressUploadImageList(fileList);
|
const compressedFileList = await compressUploadImageList(fileList);
|
||||||
const tempFileList = await AttachmentAPI.uploadTemp(compressedFileList);
|
const tempFileList = await AttachmentAPI.uploadTemp(compressedFileList);
|
||||||
for (const item of tempFileList) {
|
for (const item of tempFileList) {
|
||||||
@@ -377,8 +380,11 @@ async function handleRecordPhotoSelected(event: Event): Promise<void> {
|
|||||||
url: AttachmentAPI.getTempReadURL(item.id)
|
url: AttachmentAPI.getTempReadURL(item.id)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
photoUploading.value = false;
|
||||||
input.value = "";
|
input.value = "";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- 提交 ----------
|
// ---------- 提交 ----------
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user