Files
gao-customer-h5/src/pages/event/EventRecordEdit.vue
T

893 lines
25 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-record-edit">
<t-form class="form" label-align="left" v-entrance-transition>
<t-form-item label="客户" required-mark>
<t-input
:model-value="customerDisplayText"
class="customer"
readonly
borderless
placeholder="选择或扫码客户"
@click="!isEditMode && openSelectedCustomerList()"
>
<template v-if="!isEditMode" #suffix-icon>
<div class="actions">
<t-button
theme="primary"
variant="outline"
size="extra-small"
@click.stop="selectorVisible = true"
>选择</t-button>
<t-button
theme="primary"
size="extra-small"
@click.stop="toggleScanner"
>
<span v-text="scannerVisible ? '关闭扫码' : '扫码'" />
</t-button>
</div>
</template>
</t-input>
</t-form-item>
<div v-if="scannerVisible" class="code-scanner-wrapper">
<code-scanner
visible
:status-text="scanStatus"
active-text="正在识别客户编码,请将条码或二维码移入取景框。"
@decode="handleDecodedText"
@status-change="handleScanStatusChange"
/>
</div>
<customer-profile-card
v-if="showSelectedCustomerProfile"
class="customer-profile"
:customer="selectedCustomer"
v-entrance-transition
/>
<div v-if="isEditMode" class="edit-point-tip red">涉及积分变动的数据不能修改,已产生积分流水的登记记录不会因修改客户、事件或时间而重算;如需修改这些事实,请删除后重新创建,删除会自动回流积分。</div>
<event-selector
:model-value="selectedEventIdList"
class="event-selector"
:event-list="eventList"
:disabled="isEditMode"
selection-mode="multiple"
required-mark
@update:model-value="handleEventSelectionChange"
/>
<event-option-selector
class="option-selector"
v-model="selectedOptionSelectionMap"
:event-list="eventList"
:selected-event-id-list="selectedEventIdList"
:disabled="isSaving"
/>
<div v-if="!isEditMode && selectedNumberEventList.length" class="event-value-list">
<t-form-item
v-for="item in selectedNumberEventList"
:key="item.id"
:label="`${item.name || '未命名事件'}数值`"
:required-mark="item.requireValue === true"
label-align="top"
>
<t-input
v-model.trim="numberValueTextMap[item.id || '']"
type="number"
align="right"
borderless
:placeholder="getNumberValuePlaceholder(item)"
/>
</t-form-item>
</div>
<t-form-item v-if="!isEditMode" label="登记时间" content-align="right">
<div class="time-actions">
<t-button
theme="primary"
variant="outline"
size="extra-small"
:content="registeredAtText || '此刻'"
@click="openRegisteredAtPicker"
/>
<t-button
v-if="registeredAtText"
theme="primary"
variant="text"
size="extra-small"
content="此刻"
@click="resetRegisteredAt"
/>
</div>
</t-form-item>
<t-form-item v-else label="登记时间" content-align="right">
<span class="readonly-time" v-text="registeredAtText || '此刻'" />
</t-form-item>
<t-form-item label="登记备注">
<t-textarea
v-model.trim="note"
class="textarea"
:autosize="{ minRows: 3, maxRows: 5 }"
placeholder="登记说明"
/>
</t-form-item>
<div class="photo-part">
<media-selector
title="登记事件照片"
:required="selectedRequiredPhotoEventList.length > 0"
v-model="photoList"
v-model:photo-items="photoItemList"
:edit-mode="true"
:loading="isPhotoUploading"
@add="selectPhotoFile"
@capture-photo="capturePhoto"
@capture-video="captureVideo"
/>
</div>
</t-form>
<t-cell-group
v-if="recentRecordList.length"
title="最近登记事件列表"
class="record-list"
theme="card"
>
<t-cell
v-for="item in recentRecordList"
:key="item.id"
:title="getRecordEventName(item)"
:description="item.registeredAt ? Time.toPassedDateTime(item.registeredAt) : '-'"
arrow
hover
@click="toRecordDetail(item.id || '')"
>
<template #leftIcon>
<t-image v-if="getRecordPhotoURL(item)" :src="getRecordPhotoURL(item)" alt="事件照片" fit="cover" class="record-photo" />
</template>
</t-cell>
</t-cell-group>
<div v-else class="empty">当前客户暂无事件记录</div>
<customer-selector
v-if="!isEditMode"
v-model="selectedCustomerIdList"
v-model:visible="selectorVisible"
:selection-mode="isEditMode ? 'single' : 'multiple'"
:enable-scan="false"
@change="handleCustomerChange"
/>
<t-popup v-model="selectedCustomerListVisible" placement="bottom">
<div class="selected-customer-popup bg-white">
<div class="popup-header">
<t-button variant="text" size="small" @click="selectedCustomerListVisible = false">关闭</t-button>
<h3>已选客户</h3>
<t-button theme="primary" variant="text" size="small" @click="openCustomerSelector">选择</t-button>
</div>
<customer-list
class="list"
:customer-list="selectedCustomerList"
:removable="!isEditMode"
empty-text="暂未选择客户"
@remove="removeSelectedCustomer"
/>
</div>
</t-popup>
<input
ref="recordPhotoInputRef"
class="file-input"
type="file"
accept="*/*"
multiple
@change="handleRecordPhotoSelected"
>
<input
ref="recordPhotoCameraInputRef"
class="file-input"
type="file"
accept="image/*"
capture="environment"
@change="handleRecordPhotoSelected"
>
<input
ref="videoInputRef"
class="file-input"
type="file"
accept="video/*"
capture="environment"
@change="handleRecordPhotoSelected"
>
</div>
<t-popup v-model="registeredAtPickerVisible" placement="bottom">
<t-date-time-picker
v-if="registeredAtPickerVisible"
v-model="registeredAtDraftText"
:start="minimumDateTimeText"
:mode="['date', 'minute']"
format="YYYY-MM-DD HH:mm"
title="登记时间"
@cancel="registeredAtPickerVisible = false"
@confirm="confirmRegisteredAt"
/>
</t-popup>
</template>
<script setup lang="ts">
import { Time } from "timi-web";
import { useUserStore } from "timi-web-api";
import { Message } from "@/utils/message";
import CustomerAPI from "@/api/CustomerAPI";
import EventAPI from "@/api/EventAPI";
import EventRecordAPI from "@/api/EventRecordAPI";
import CodeScanner from "@/components/CodeScanner.vue";
import CustomerList from "@/components/CustomerList.vue";
import CustomerProfileCard from "@/components/CustomerProfileCard.vue";
import CustomerSelector from "@/components/CustomerSelector.vue";
import EventOptionSelector from "@/components/EventOptionSelector.vue";
import MediaSelector from "@/components/mediaSelector/MediaSelector.vue";
import EventSelector from "@/components/EventSelector.vue";
import { useEventRecordPhoto } from "@/pages/event/component/useEventRecordPhoto";
import { useNavBarStore } from "@/store/navBarStore";
import { useSettingStore } from "@/store/settingStore";
import type { Customer } from "@/types/Customer";
import type { EventItem, EventOptionSelectionMap } from "@/types/EventItem";
import type { EventRecord } from "@/types/EventRecord";
import { isRetiredEvent } from "@/utils/event";
import { buildEventOptionSelectionMap, resolveSelectedOptionGroupList, validateEventOptionSelection } from "@/utils/eventOption";
import { getEventRecordPhotoURL, hasPhotoAttachment } from "@/utils/eventRecord";
import { getEventNumberValuePlaceholder, resolveEventNumberValue } from "@/utils/eventValue";
import { permissionCodes } from "@/utils/permission";
defineOptions({
name: "EventRecordEdit"
});
const router = useRouter();
const route = useRoute();
const settingStore = useSettingStore();
const userStore = useUserStore();
const navBarStore = useNavBarStore();
// ---------- 路由权限 ----------
const routeId = computed(() => String(route.params.id || ""));
const isEditMode = computed(() => !!routeId.value);
const pageAccess = computed(() => isEditMode.value ? permissionCodes.eventRecord.update : permissionCodes.eventRecord.create);
const saveButtonText = computed(() => isEditMode.value ? "保存" : "提交");
// ---------- 客户 ----------
const customerCode = ref("");
const selectorVisible = ref(false);
const selectedCustomerListVisible = ref(false);
const selectedCustomerIdList = ref<string[]>([]);
const selectedCustomerList = ref<Customer[]>([]);
const selectedCustomer = computed(() => selectedCustomerList.value[0]);
const showSelectedCustomerProfile = computed(() => selectedCustomerList.value.length === 1 && !!selectedCustomer.value?.id);
const customerDisplayText = computed(() => {
if (1 < selectedCustomerList.value.length) {
return `已选择 ${selectedCustomerList.value.length} 位客户`;
}
return selectedCustomer.value?.name || selectedCustomer.value?.code || customerCode.value;
});
async function handleCustomerChange(value: string | string[]): Promise<void> {
selectedCustomerIdList.value = Array.isArray(value) ? value : [value];
await loadSelectedCustomerList();
await loadRecentRecordList();
}
async function loadSelectedCustomerList(): Promise<void> {
const customerList = await Promise.all(selectedCustomerIdList.value.map(id => CustomerAPI.detail(id)));
selectedCustomerList.value = customerList.filter(item => !!item.id);
const firstCustomer = selectedCustomer.value;
customerCode.value = firstCustomer?.code || "";
scanCode.value = firstCustomer?.code || "";
}
function openSelectedCustomerList(): void {
selectedCustomerListVisible.value = true;
}
async function openCustomerSelector(): Promise<void> {
selectedCustomerListVisible.value = false;
await nextTick();
selectorVisible.value = true;
}
async function removeSelectedCustomer(customer: Customer): Promise<void> {
if (!customer.id || isEditMode.value) {
return;
}
selectedCustomerIdList.value = selectedCustomerIdList.value.filter(id => id !== customer.id);
selectedCustomerList.value = selectedCustomerList.value.filter(item => item.id !== customer.id);
await loadRecentRecordList();
}
async function toRecordDetail(id: string): Promise<void> {
if (!id) {
return;
}
await router.push(`/management/registration-event-records/${id}`);
}
async function loadSelectedCustomer(): Promise<void> {
const code = customerCode.value.trim();
if (!code) {
selectedCustomerList.value = [];
selectedCustomerIdList.value = [];
recentRecordList.value = [];
return;
}
const customer = await CustomerAPI.findByCode(code);
if (!customer) {
selectedCustomerList.value = [];
selectedCustomerIdList.value = [];
recentRecordList.value = [];
scanStatus.value = `客户 ${code} 不存在,请先建档。`;
return;
}
if (customer.id) {
selectedCustomerIdList.value = isEditMode.value
? [customer.id]
: [...new Set([...selectedCustomerIdList.value, customer.id])];
selectedCustomerList.value = isEditMode.value
? [customer]
: [...selectedCustomerList.value.filter(item => item.id !== customer.id), customer];
}
await loadRecentRecordList();
}
// ---------- 最近记录 ----------
const recentRecordList = ref<EventRecord[]>([]);
async function loadRecentRecordList(): Promise<void> {
const customerId = selectedCustomer.value?.id;
if (!customerId) {
recentRecordList.value = [];
return;
}
const endAt = Time.toTimestamp(new Date());
const result = await EventRecordAPI.list({
index: 0,
size: 5,
customerId,
beginAt: 0,
endAt
});
recentRecordList.value = (result.list || [])
.sort((left, right) => (right.registeredAt || 0) - (left.registeredAt || 0))
.slice(0, 5);
}
function getRecordEventName(record: EventRecord): string {
return record.event?.name || (record.eventId ? eventNameMap.value.get(record.eventId) : "") || "未知事件";
}
function getRecordPhotoURL(record: EventRecord): string {
return getEventRecordPhotoURL(record, 64);
}
// ---------- 扫码 ----------
const scanCode = ref(customerCode.value);
const scannerVisible = ref(false);
const scanStatus = ref("点击扫码开始识别。");
let latestDecodedText = "";
async function toggleScanner(): Promise<void> {
scannerVisible.value = !scannerVisible.value;
if (scannerVisible.value) {
latestDecodedText = "";
}
}
async function handleDecodedText(rawText: string): Promise<void> {
const normalizedText = rawText.trim();
if (!normalizedText || normalizedText === latestDecodedText) {
return;
}
latestDecodedText = normalizedText;
scanCode.value = normalizedText;
await settingStore.playScanSuccessSound();
await applyScanCode();
}
async function applyScanCode(): Promise<void> {
customerCode.value = scanCode.value.trim();
scanStatus.value = `已识别 ${customerCode.value},正在加载客户档案。`;
await loadSelectedCustomer();
if (!selectedCustomer.value) {
return;
}
scanStatus.value = `已识别 ${selectedCustomer.value.code || customerCode.value},已自动选中 ${selectedCustomer.value.name || "未命名客户"}。`;
scannerVisible.value = false;
}
function handleScanStatusChange(value: string): void {
scanStatus.value = value;
}
// ---------- 事件 ----------
const eventList = ref<EventItem[]>([]);
const selectedEventIdList = ref<string[]>([]);
const selectedOptionSelectionMap = reactive<EventOptionSelectionMap>({});
const numberValueTextMap = reactive<Record<string, string>>({});
const selectedNumberEventList = computed(() => selectedEventIdList.value
.map(eventId => eventList.value.find(item => item.id === eventId))
.filter((item): item is EventItem => item?.valueType === "NUMBER"));
const selectedRequiredPhotoEventList = computed(() => selectedEventIdList.value
.map(eventId => eventList.value.find(item => item.id === eventId))
.filter((item): item is EventItem => item?.requirePhoto === true));
const eventNameMap = computed(() => {
const map = new Map<string, string>();
for (const item of eventList.value) {
if (item.id) {
map.set(item.id, item.name || "未命名事件");
}
}
return map;
});
function appendRecordEvent(record: EventRecord): void {
if (!record.event?.id || eventList.value.some(item => item.id === record.event?.id)) {
return;
}
eventList.value = [record.event, ...eventList.value];
}
function handleEventSelectionChange(eventIdList: string[]): void {
selectedEventIdList.value = eventIdList;
}
function resolveRecordOptionGroupList(eventId: string) {
const event = eventList.value.find(item => item.id === eventId);
return event ? resolveSelectedOptionGroupList(event, selectedOptionSelectionMap) : [];
}
function getNumberValuePlaceholder(event: EventItem): string {
return getEventNumberValuePlaceholder(event);
}
function resolveNumberValue(eventId: string): number | undefined {
const valueText = numberValueTextMap[eventId] || "";
if (!valueText.trim()) {
return undefined;
}
const event = eventList.value.find(item => item.id === eventId);
return event ? resolveEventNumberValue(event, valueText) : undefined;
}
function validateNumberValueList(): boolean {
for (const eventId of selectedEventIdList.value) {
const event = eventList.value.find(item => item.id === eventId);
if (!event) {
continue;
}
const valueText = numberValueTextMap[eventId] || "";
if (event.valueType !== "NUMBER") {
if (valueText.trim()) {
Message.warning(`${event.name || "当前事件"}不能提交数值`);
return false;
}
continue;
}
const numberValue = resolveNumberValue(eventId);
if (valueText.trim() && numberValue === undefined) {
Message.warning(`${event.name || "当前事件"}数值格式不正确,请检查单位和小数位`);
return false;
}
if (event.requireValue && numberValue === undefined) {
Message.warning(`请填写${event.name || "当前事件"}数值`);
return false;
}
}
return true;
}
// ---------- 照片 ----------
const {
recordPhotoInputRef,
recordPhotoCameraInputRef,
videoInputRef,
photoItemList,
isPhotoUploading,
photoList,
capturePhoto,
selectPhotoFile,
captureVideo,
handleRecordPhotoSelected,
setExistingPhotoList,
buildTempFileIdList,
buildPhotoAttachmentList
} = useEventRecordPhoto();
// ---------- 登记时间 ----------
const minimumDateTimeText = "1970-01-01 00:00";
const registeredAtText = ref("");
const registeredAtDraftText = ref("");
const registeredAtPickerVisible = ref(false);
function openRegisteredAtPicker(): void {
registeredAtDraftText.value = registeredAtText.value || Time.formatDateTime(new Date());
registeredAtPickerVisible.value = true;
}
function confirmRegisteredAt(value?: string | number): void {
registeredAtText.value = String(value || registeredAtDraftText.value);
registeredAtPickerVisible.value = false;
}
function resetRegisteredAt(): void {
registeredAtText.value = "";
registeredAtDraftText.value = "";
}
function buildRegisteredAt(): number {
return Time.toTimestamp(parseDateTime(registeredAtText.value));
}
function parseDateTime(value: string): Date {
if (!value) {
return new Date();
}
const [dateText, timeText = "00:00"] = value.split(" ");
const [year, month, day] = dateText.split("-").map(Number);
const [hour, minute] = timeText.split(":").map(Number);
return new Date(year, month - 1, day, hour || 0, minute || 0, 0, 0);
}
// ---------- 提交 ----------
const note = ref("");
const isSaving = ref(false);
const editingRecord = ref<EventRecord>();
async function submitRecord(): Promise<void> {
if (isSaving.value) {
return;
}
if (isPhotoUploading.value) {
Message.warning("照片正在上传,请稍候");
return;
}
if (!validateForm()) {
return;
}
isSaving.value = true;
try {
if (isEditMode.value) {
await updateRecord();
return;
}
const success = await submitEventRecords();
if (!success) {
return;
}
resetCreateForm();
await loadRecentRecordList();
await settingStore.playRegisterSuccessSound();
Message.success("登记已提交");
await router.replace("/customers");
} finally {
isSaving.value = false;
}
}
async function submitEventRecords(): Promise<boolean> {
const customerIdList = selectedCustomerList.value.map(customer => customer.id).filter((item): item is string => !!item);
// 后端批量接口仍按选择顺序逐条处理,并为重复临时附件生成副本。
await EventRecordAPI.createBatch(selectedEventIdList.value.map(eventId => ({
eventId,
customerIdList,
selectedOptionGroupList: resolveRecordOptionGroupList(eventId),
remark: note.value || "客户现场登记",
registeredAt: buildRegisteredAt(),
numberValue: resolveNumberValue(eventId),
attachmentList: buildTempFileIdList().map(tempFileId => ({
tempFileId,
attachType: "PHOTO"
}))
})));
return true;
}
function resetCreateForm(): void {
selectedCustomerList.value = [];
selectedCustomerIdList.value = [];
note.value = "";
photoItemList.value = [];
for (const eventId of Object.keys(numberValueTextMap)) {
delete numberValueTextMap[eventId];
}
for (const eventId of Object.keys(selectedOptionSelectionMap)) {
delete selectedOptionSelectionMap[eventId];
}
}
function validateForm(): boolean {
if (!selectedCustomerList.value.some(item => !!item.id)) {
Message.warning("请先选择或扫码客户");
return false;
}
if (!selectedEventIdList.value.length) {
Message.warning("请至少选择一个登记事件");
return false;
}
for (const eventId of selectedEventIdList.value) {
const event = eventList.value.find(item => item.id === eventId);
if (!event) {
continue;
}
const optionError = validateEventOptionSelection(event, selectedOptionSelectionMap);
if (optionError) {
Message.warning(optionError);
return false;
}
}
if (!validateRequiredPhotoList()) {
return false;
}
if (!isEditMode.value && !validateNumberValueList()) {
return false;
}
return true;
}
function validateRequiredPhotoList(): boolean {
const event = selectedRequiredPhotoEventList.value[0];
if (event && !hasPhotoAttachment(photoItemList.value)) {
Message.warning(`请上传${event.name || "当前事件"}照片`);
return false;
}
return true;
}
async function updateRecord(): Promise<void> {
if (!editingRecord.value?.id || !editingRecord.value.customerId || !editingRecord.value.eventId) {
Message.warning("登记事件记录不存在");
return;
}
await EventRecordAPI.update({
id: editingRecord.value.id,
customerId: editingRecord.value.customerId,
eventId: editingRecord.value.eventId,
selectedOptionGroupList: resolveRecordOptionGroupList(editingRecord.value.eventId),
remark: note.value,
registeredAt: editingRecord.value.registeredAt,
attachmentList: buildPhotoAttachmentList()
});
Message.success("记录已保存");
await router.replace(`/management/registration-event-records/${editingRecord.value.id}`);
}
// ---------- 页面数据 ----------
async function loadPageData(): Promise<void> {
// 套餐购买不再走事件登记,退役消费事件也不可登记
eventList.value = (await EventAPI.listValid()).filter(item => !isRetiredEvent(item));
if (isEditMode.value) {
await loadRecordForEdit();
return;
}
const defaultEvent = eventList.value[0];
selectedEventIdList.value = defaultEvent?.id ? [defaultEvent.id] : [];
}
async function loadRecordForEdit(): Promise<void> {
const record = await EventRecordAPI.detail(routeId.value);
if (isRetiredEvent(record.event)) {
Message.warning("退役事件记录仅支持历史查看,不能编辑");
await router.replace({ name: "EventRecordDetail", params: { id: routeId.value } });
return;
}
editingRecord.value = record;
appendRecordEvent(record);
selectedCustomerList.value = record.customer ? [record.customer] : [];
customerCode.value = record.customer?.code || record.customerId || "";
selectedCustomerIdList.value = record.customer?.id ? [record.customer.id] : [];
selectedEventIdList.value = record.eventId ? [record.eventId] : [];
for (const eventId of Object.keys(selectedOptionSelectionMap)) {
delete selectedOptionSelectionMap[eventId];
}
if (record.eventId) {
Object.assign(selectedOptionSelectionMap, buildEventOptionSelectionMap(record.eventId, record.selectedOptionGroupList));
}
registeredAtText.value = record.registeredAt ? Time.formatDateTime(new Date(record.registeredAt)) : "";
note.value = record.remark || "";
setExistingPhotoList(record.attachmentList);
await loadRecentRecordList();
}
onMounted(loadPageData);
onMounted(() => {
navBarStore.clearRight();
if (userStore.hasPermissions(pageAccess.value)) {
navBarStore.setRightText(saveButtonText.value, "var(--td-brand-color)", submitRecord);
}
});
onUnmounted(() => {
navBarStore.clearRight();
});
watch(customerCode, async (value) => {
if (!value) {
return;
}
scanCode.value = value;
});
</script>
<style scoped lang="less">
.registration-record-edit {
gap: 1rem;
display: flex;
padding: calc(var(--app-nav-height) + 1rem) 0 1rem 0;
flex-direction: column;
.form,
.record-panel {
background: rgba(255, 255, 255, .92);
box-shadow: var(--td-shadow-1);
border-radius: 1.4rem;
}
.form {
padding: .5rem 0;
background: rgba(255, 255, 255, .92);
box-shadow: var(--td-shadow-1);
border-radius: 1.4rem;
.customer {
.actions {
gap: .5rem;
display: flex;
}
}
.code-scanner-wrapper,
.photo-part {
padding: 1rem;
}
.textarea {
width: 100%;
}
.customer-profile {
margin: 1rem;
}
.event-selector {
padding: 0 1rem;
margin-top: 1rem;
}
.event-value-list {
padding: 0 1rem;
.t-form-item {
padding: 0;
}
}
.option-selector {
margin: 1rem 0 1rem 1rem;
}
.time-actions {
gap: .5rem;
display: flex;
align-items: center;
justify-content: flex-end;
}
.edit-point-tip {
padding: 0 1rem 1rem;
font-size: .8125rem;
margin-top: .25rem;
line-height: 1.5;
}
.readonly-time {
color: #52607a;
}
}
.record-photo {
width: 3.5rem;
height: 3.5rem;
border-radius: .875rem;
}
.empty {
color: #52607a;
margin: 0 1rem;
padding: 1rem;
text-align: center;
background: #f7faff;
border-radius: 1rem;
}
.file-input {
display: none;
}
}
.selected-customer-popup {
height: 70vh;
overflow: auto;
padding-top: 1rem;
border-radius: 1.25rem 1.25rem 0 0;
.popup-header {
padding: 0 1rem;
display: flex;
align-items: center;
justify-content: space-between;
h3 {
margin: 0;
}
}
.list {
margin-top: 1rem;
}
}
:global(html.theme-dark) {
.registration-record-edit {
:deep(.form-item .t-form__label),
.selector-text p,
.empty-block {
color: #94a3b8;
}
.form,
.record-panel,
.selected-customer-popup {
background: rgba(15, 23, 42, .88);
box-shadow: 0 .75rem 2rem rgba(2, 6, 23, .32);
}
:deep(.textarea .t-textarea__inner) {
color: #e2e8f0;
background: rgba(15, 23, 42, .72);
border-color: rgba(148, 163, 184, .16);
}
.empty-block {
background: rgba(30, 41, 59, .9);
}
.selector-text strong {
color: #e2e8f0;
}
.selector-action {
color: #93c5fd;
}
.edit-point-tip,
.readonly-time {
color: #fbbf24;
}
}
}
</style>