fix store business day
This commit is contained in:
+5
-1
@@ -22,6 +22,10 @@ async function update(store: Store): Promise<void> {
|
|||||||
await axios.post(`${STORE_BASE_URI}/update`, store);
|
await axios.post(`${STORE_BASE_URI}/update`, store);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function sort(idList: string[]): Promise<void> {
|
||||||
|
await axios.post(`${STORE_BASE_URI}/sort`, idList);
|
||||||
|
}
|
||||||
|
|
||||||
async function remove(id: string): Promise<void> {
|
async function remove(id: string): Promise<void> {
|
||||||
await axios.post(`${STORE_BASE_URI}/delete`, undefined, {
|
await axios.post(`${STORE_BASE_URI}/delete`, undefined, {
|
||||||
params: { id }
|
params: { id }
|
||||||
@@ -73,6 +77,7 @@ export default {
|
|||||||
detail,
|
detail,
|
||||||
create,
|
create,
|
||||||
update,
|
update,
|
||||||
|
sort,
|
||||||
remove,
|
remove,
|
||||||
exportXSSF,
|
exportXSSF,
|
||||||
businessDayList,
|
businessDayList,
|
||||||
@@ -82,4 +87,3 @@ export default {
|
|||||||
updateBusinessDay,
|
updateBusinessDay,
|
||||||
removeBusinessDay
|
removeBusinessDay
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<section class="total">
|
<section class="total">
|
||||||
<span class="gray">客户总数:</span>
|
<span class="gray">客户总数:</span>
|
||||||
<span class="bold" v-text="pageResult.total" />
|
<span class="text-bold" v-text="pageResult.total" />
|
||||||
</section>
|
</section>
|
||||||
<t-pull-down-refresh
|
<t-pull-down-refresh
|
||||||
v-model="isRefreshing"
|
v-model="isRefreshing"
|
||||||
|
|||||||
@@ -0,0 +1,437 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="store"
|
||||||
|
v-can="permissionCodes.store.read"
|
||||||
|
class="store-business-day-calendar"
|
||||||
|
>
|
||||||
|
<header class="summary bg-white">
|
||||||
|
<header class="header">
|
||||||
|
<div class="text-bold" v-text="store.name" />
|
||||||
|
<t-tag variant="outline" :content="store.code" />
|
||||||
|
</header>
|
||||||
|
<div class="address gray">
|
||||||
|
<span>地址:</span>
|
||||||
|
<span v-text="store.address" />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div class="calendar-card">
|
||||||
|
<t-calendar
|
||||||
|
:key="calendarRenderKey"
|
||||||
|
class="calendar"
|
||||||
|
:value="selectedCalendarDate"
|
||||||
|
:min-date="calendarMinDate"
|
||||||
|
:max-date="calendarMaxDate"
|
||||||
|
:format="calendarFormat"
|
||||||
|
switch-mode="month"
|
||||||
|
:use-popup="false"
|
||||||
|
@panel-change="handlePanelChange"
|
||||||
|
@select="selectCalendarDate"
|
||||||
|
/>
|
||||||
|
<div v-if="isCalendarLoading" class="loading" v-text="'正在加载当月营业日'" />
|
||||||
|
<div v-else-if="!businessDayList.length" class="empty" v-text="'当月暂无营业日配置,点击日期即可新增'" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<t-popup v-model="editorVisible" placement="bottom">
|
||||||
|
<div class="editor">
|
||||||
|
<div class="header">
|
||||||
|
<div class="text-bold" v-text="editingId ? '编辑营业日' : '新增营业日'" />
|
||||||
|
<div v-text="formModel.dateText" />
|
||||||
|
</div>
|
||||||
|
<t-form label-align="left">
|
||||||
|
<t-form-item label="状态">
|
||||||
|
<t-radio-group v-model="formModel.openStatus" variant="default-filled">
|
||||||
|
<t-radio :block="false" borderless value="OPEN">营业</t-radio>
|
||||||
|
<t-radio :block="false" borderless value="CLOSED">休息</t-radio>
|
||||||
|
</t-radio-group>
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item
|
||||||
|
class="reason"
|
||||||
|
:class="{ 'is-hidden': formModel.openStatus !== 'CLOSED' }"
|
||||||
|
label="原因"
|
||||||
|
>
|
||||||
|
<t-input
|
||||||
|
v-model.trim="formModel.reason"
|
||||||
|
:disabled="formModel.openStatus !== 'CLOSED'"
|
||||||
|
placeholder="休息原因或备注"
|
||||||
|
borderless
|
||||||
|
/>
|
||||||
|
</t-form-item>
|
||||||
|
</t-form>
|
||||||
|
<span class="hint orange">切换营业或休息可能影响连续登记统计,系统重排营业日事件序列可能需要较长时间。</span>
|
||||||
|
<div class="actions">
|
||||||
|
<t-button
|
||||||
|
v-if="editingId"
|
||||||
|
v-can="permissionCodes.store.delete"
|
||||||
|
class="action"
|
||||||
|
theme="danger"
|
||||||
|
variant="outline"
|
||||||
|
:loading="isDeleting"
|
||||||
|
@click="confirmRemoveDay"
|
||||||
|
>删除</t-button>
|
||||||
|
<t-button
|
||||||
|
v-can="savePermissionCode"
|
||||||
|
class="action"
|
||||||
|
theme="primary"
|
||||||
|
:loading="isSaving"
|
||||||
|
@click="saveDay"
|
||||||
|
>保存</t-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t-popup>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { DialogPlugin, Toast, type CalendarProps, type TDate } from "tdesign-mobile-vue";
|
||||||
|
import StoreAPI from "@/api/StoreAPI";
|
||||||
|
import type { Store, StoreBusinessDay, StoreBusinessDayOpenStatus } from "@/types/Store";
|
||||||
|
import { permissionCodes } from "@/utils/permission";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "StoreBusinessDayCalendar"
|
||||||
|
});
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const storeId = computed(() => String(route.params.id || ""));
|
||||||
|
|
||||||
|
// ---------- 门店信息 ----------
|
||||||
|
|
||||||
|
const store = ref<Store>();
|
||||||
|
|
||||||
|
// ---------- 日历数据 ----------
|
||||||
|
|
||||||
|
const today = new Date();
|
||||||
|
const calendarMinDate = new Date(today.getFullYear() - 2, 0, 1);
|
||||||
|
const calendarMaxDate = new Date(today.getFullYear() + 2, 11, 31);
|
||||||
|
const selectedCalendarDate = ref<Date>(today);
|
||||||
|
const calendarMonth = reactive({
|
||||||
|
year: today.getFullYear(),
|
||||||
|
month: today.getMonth() + 1
|
||||||
|
});
|
||||||
|
const businessDayList = ref<StoreBusinessDay[]>([]);
|
||||||
|
const isCalendarLoading = ref(false);
|
||||||
|
const calendarRenderKey = ref(0);
|
||||||
|
let calendarRequestId = 0;
|
||||||
|
|
||||||
|
const businessDayMap = computed(() => new Map(
|
||||||
|
businessDayList.value
|
||||||
|
.filter(item => item.dateValue)
|
||||||
|
.map(item => [item.dateValue as number, item])
|
||||||
|
));
|
||||||
|
|
||||||
|
const calendarFormat = computed<NonNullable<CalendarProps["format"]>>(() => {
|
||||||
|
const dayMap = businessDayMap.value;
|
||||||
|
return (dateItem: TDate) => {
|
||||||
|
const day = dayMap.get(toDateValue(dateItem.date));
|
||||||
|
if (!day) {
|
||||||
|
return dateItem;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...dateItem,
|
||||||
|
className: `business-day-${day.openStatus!.toLowerCase()}`,
|
||||||
|
suffix: day.openStatus === "OPEN" ? "营" : "休"
|
||||||
|
};
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadCalendar(year: number, month: number): Promise<void> {
|
||||||
|
const requestId = ++calendarRequestId;
|
||||||
|
isCalendarLoading.value = true;
|
||||||
|
try {
|
||||||
|
const beginDate = new Date(year, month - 1, 1);
|
||||||
|
const endDate = new Date(year, month, 0);
|
||||||
|
const result = await StoreAPI.businessDayCalendar(storeId.value, toDateValue(beginDate), toDateValue(endDate));
|
||||||
|
if (requestId === calendarRequestId) {
|
||||||
|
businessDayList.value = result || [];
|
||||||
|
calendarRenderKey.value += 1;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (requestId === calendarRequestId) {
|
||||||
|
isCalendarLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handlePanelChange(context: { year: number; month: number }): Promise<void> {
|
||||||
|
if (context.year === calendarMonth.year && context.month === calendarMonth.month) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
calendarMonth.year = context.year;
|
||||||
|
calendarMonth.month = context.month;
|
||||||
|
selectedCalendarDate.value = new Date(context.year, context.month - 1, 1);
|
||||||
|
try {
|
||||||
|
await loadCalendar(context.year, context.month);
|
||||||
|
} catch {
|
||||||
|
Toast.error("营业日加载失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 编辑 ----------
|
||||||
|
|
||||||
|
const editorVisible = ref(false);
|
||||||
|
const isSaving = ref(false);
|
||||||
|
const isDeleting = ref(false);
|
||||||
|
const editingId = ref("");
|
||||||
|
const formModel = reactive({
|
||||||
|
dateText: "",
|
||||||
|
openStatus: "OPEN" as StoreBusinessDayOpenStatus,
|
||||||
|
reason: "正常休息"
|
||||||
|
});
|
||||||
|
const savePermissionCode = computed(() => editingId.value ? permissionCodes.store.update : permissionCodes.store.create);
|
||||||
|
|
||||||
|
function openDayEditor(day?: StoreBusinessDay, dateValue = toDateValue(new Date())): void {
|
||||||
|
editingId.value = day?.id || "";
|
||||||
|
Object.assign(formModel, {
|
||||||
|
dateText: formatDateValue(day?.dateValue || dateValue),
|
||||||
|
openStatus: day?.openStatus || "OPEN",
|
||||||
|
reason: day ? day.reason : "正常休息"
|
||||||
|
});
|
||||||
|
editorVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectCalendarDate(date: Date): void {
|
||||||
|
selectedCalendarDate.value = date;
|
||||||
|
const dateValue = toDateValue(date);
|
||||||
|
openDayEditor(businessDayMap.value.get(dateValue), dateValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveDay(): Promise<void> {
|
||||||
|
if (!validateDay() || isSaving.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isSaving.value = true;
|
||||||
|
try {
|
||||||
|
const day: StoreBusinessDay = {
|
||||||
|
id: editingId.value || undefined,
|
||||||
|
storeId: storeId.value,
|
||||||
|
dateValue: parseDateValue(formModel.dateText),
|
||||||
|
openStatus: formModel.openStatus,
|
||||||
|
reason: formModel.openStatus === "CLOSED" ? formModel.reason || undefined : undefined
|
||||||
|
};
|
||||||
|
if (editingId.value) {
|
||||||
|
await StoreAPI.updateBusinessDay(day);
|
||||||
|
} else {
|
||||||
|
await StoreAPI.createBusinessDay(day);
|
||||||
|
}
|
||||||
|
Toast.success("营业日已保存");
|
||||||
|
editorVisible.value = false;
|
||||||
|
await loadCalendar(calendarMonth.year, calendarMonth.month);
|
||||||
|
} catch {
|
||||||
|
Toast.error("营业日保存失败");
|
||||||
|
} finally {
|
||||||
|
isSaving.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateDay(): boolean {
|
||||||
|
if (!formModel.dateText) {
|
||||||
|
Toast.warning("请选择日期");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 删除 ----------
|
||||||
|
|
||||||
|
function confirmRemoveDay(): void {
|
||||||
|
if (!editingId.value || isDeleting.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dayId = editingId.value;
|
||||||
|
const dialog = DialogPlugin.confirm({
|
||||||
|
title: "删除营业日",
|
||||||
|
content: `确定删除 ${formModel.dateText} 的营业日配置吗?已有登记记录的营业日系统会拒绝删除`,
|
||||||
|
cancelBtn: "取消",
|
||||||
|
confirmBtn: "删除",
|
||||||
|
onCancel() {
|
||||||
|
dialog.destroy();
|
||||||
|
},
|
||||||
|
async onConfirm() {
|
||||||
|
dialog.destroy();
|
||||||
|
await removeDay(dayId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeDay(id: string): Promise<void> {
|
||||||
|
isDeleting.value = true;
|
||||||
|
try {
|
||||||
|
await StoreAPI.removeBusinessDay(id);
|
||||||
|
Toast.success("营业日已删除");
|
||||||
|
editorVisible.value = false;
|
||||||
|
await loadCalendar(calendarMonth.year, calendarMonth.month);
|
||||||
|
} catch {
|
||||||
|
Toast.error("营业日删除失败");
|
||||||
|
} finally {
|
||||||
|
isDeleting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 日期 ----------
|
||||||
|
|
||||||
|
function formatDateValue(value?: number): string {
|
||||||
|
if (!value) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = String(value);
|
||||||
|
return `${text.slice(0, 4)}-${text.slice(4, 6)}-${text.slice(6, 8)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseDateValue(text: string): number {
|
||||||
|
return Number(text.replaceAll("-", ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
function toDateValue(date: Date): number {
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(date.getDate()).padStart(2, "0");
|
||||||
|
return Number(`${year}${month}${day}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
const [currentStore] = await Promise.all([
|
||||||
|
StoreAPI.detail(storeId.value),
|
||||||
|
loadCalendar(calendarMonth.year, calendarMonth.month)
|
||||||
|
]);
|
||||||
|
store.value = currentStore;
|
||||||
|
} catch {
|
||||||
|
Toast.error("营业日数据加载失败");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.store-business-day-calendar {
|
||||||
|
gap: .75rem;
|
||||||
|
display: grid;
|
||||||
|
padding: calc(var(--app-nav-height) + 1rem) 1rem 6rem;
|
||||||
|
|
||||||
|
.summary {
|
||||||
|
gap: .75rem;
|
||||||
|
display: flex;
|
||||||
|
padding: .75rem;
|
||||||
|
border-radius: .75rem;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address {
|
||||||
|
font-size: .875rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend {
|
||||||
|
gap: .375rem;
|
||||||
|
display: flex;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-card {
|
||||||
|
|
||||||
|
:deep(.t-calendar-header) {
|
||||||
|
padding-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.t-calendar) {
|
||||||
|
z-index: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: .75rem;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__months {
|
||||||
|
height: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__dates-item {
|
||||||
|
height: 40px;
|
||||||
|
align-items: unset;
|
||||||
|
|
||||||
|
&.business-day-open {
|
||||||
|
--td-calendar-item-suffix-color: var(--td-brand-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.business-day-closed .t-calendar__dates-item-suffix {
|
||||||
|
--td-calendar-item-suffix-color: var(--td-error-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.business-day-open),
|
||||||
|
:deep(.business-day-closed) {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.business-day-closed .t-calendar__dates-item-suffix) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading,
|
||||||
|
.empty {
|
||||||
|
color: #52607a;
|
||||||
|
padding: .75rem 1rem;
|
||||||
|
font-size: .8125rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor {
|
||||||
|
gap: .75rem;
|
||||||
|
display: flex;
|
||||||
|
padding: 1rem;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reason.is-hidden {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
text-indent: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
gap: .75rem;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.action {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html.theme-dark) {
|
||||||
|
.store-business-day-calendar {
|
||||||
|
.summary,
|
||||||
|
.calendar-card,
|
||||||
|
.editor {
|
||||||
|
background: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary span,
|
||||||
|
.loading,
|
||||||
|
.empty,
|
||||||
|
.editor .hint {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary strong,
|
||||||
|
.editor h3 {
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,406 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div v-can="permissionCodes.store.read" class="business-day-list">
|
|
||||||
<header class="summary">
|
|
||||||
<div>
|
|
||||||
<strong v-text="storeName" />
|
|
||||||
<span v-text="storeCodeText" />
|
|
||||||
</div>
|
|
||||||
<t-tag theme="default" variant="outline" content="无记录表示未配置" />
|
|
||||||
</header>
|
|
||||||
<t-pull-down-refresh
|
|
||||||
v-model="isRefreshing"
|
|
||||||
:refresh-timeout="30000"
|
|
||||||
@refresh="refresh"
|
|
||||||
@scrolltolower="fetch"
|
|
||||||
>
|
|
||||||
<div v-for="item in pageResult.list" :key="item.id" class="item">
|
|
||||||
<div class="content" @click="openDayEditor(item)">
|
|
||||||
<div class="header">
|
|
||||||
<span class="date" v-text="formatDateValue(item.dateValue)" />
|
|
||||||
<t-tag size="small" :theme="getBusinessDayStatusTheme(item.openStatus)" variant="outline" :content="getBusinessDayStatusText(item.openStatus)" />
|
|
||||||
</div>
|
|
||||||
<span class="meta" v-text="item.openStatus === 'OPEN' ? `营业日序号:${item.businessDayNo || '-'}` : `原因:${item.reason || '-'}`" />
|
|
||||||
</div>
|
|
||||||
<t-button v-can="permissionCodes.store.delete" class="delete" shape="circle" theme="danger" variant="text" @click="confirmRemoveDay(item)">
|
|
||||||
<t-icon name="delete" />
|
|
||||||
</t-button>
|
|
||||||
</div>
|
|
||||||
<div v-if="!pageResult.list.length" class="empty-block" v-text="isLoading ? '正在加载营业日' : '暂无已配置营业日'" />
|
|
||||||
<div v-if="pageResult.list.length" class="load-state" v-text="isNoMore ? '已经到底了' : '上拉加载更多'" />
|
|
||||||
</t-pull-down-refresh>
|
|
||||||
<t-button v-can="permissionCodes.store.create" class="add" theme="primary" @click="openDayEditor()">
|
|
||||||
<template #icon>
|
|
||||||
<t-icon name="add" />
|
|
||||||
</template>
|
|
||||||
新增营业日
|
|
||||||
</t-button>
|
|
||||||
<t-popup v-model="editorVisible" placement="bottom">
|
|
||||||
<div class="editor">
|
|
||||||
<h3 v-text="editingId ? '编辑营业日' : '新增营业日'" />
|
|
||||||
<t-form label-align="left">
|
|
||||||
<t-form-item label="日期">
|
|
||||||
<button class="picker-btn" type="button" @click="openDatePicker">
|
|
||||||
<span v-text="formModel.dateText || '选择日期'" />
|
|
||||||
<t-icon name="calendar" />
|
|
||||||
</button>
|
|
||||||
</t-form-item>
|
|
||||||
<t-form-item label="状态">
|
|
||||||
<t-radio-group v-model="formModel.openStatus" variant="default-filled">
|
|
||||||
<t-radio :block="false" borderless value="OPEN">营业</t-radio>
|
|
||||||
<t-radio :block="false" borderless value="CLOSED">休息</t-radio>
|
|
||||||
</t-radio-group>
|
|
||||||
</t-form-item>
|
|
||||||
<t-form-item label="原因">
|
|
||||||
<t-input v-model.trim="formModel.reason" placeholder="休息原因或备注" borderless />
|
|
||||||
</t-form-item>
|
|
||||||
</t-form>
|
|
||||||
<p class="hint">切换营业或休息可能影响连续登记统计,营业日序号由后端自动维护。</p>
|
|
||||||
<t-button v-can="savePermissionCode" theme="primary" block :loading="isSaving" @click="saveDay">保存</t-button>
|
|
||||||
</div>
|
|
||||||
</t-popup>
|
|
||||||
<t-popup v-model="datePickerVisible" placement="bottom">
|
|
||||||
<t-date-time-picker
|
|
||||||
v-if="datePickerVisible"
|
|
||||||
v-model="datePickerDraft"
|
|
||||||
:mode="['date']"
|
|
||||||
format="YYYY-MM-DD"
|
|
||||||
title="营业日期"
|
|
||||||
@cancel="datePickerVisible = false"
|
|
||||||
@confirm="confirmDatePicker"
|
|
||||||
/>
|
|
||||||
</t-popup>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { DialogPlugin, Toast } from "tdesign-mobile-vue";
|
|
||||||
import { type Page, type PageResult } from "timi-web";
|
|
||||||
import StoreAPI from "@/api/StoreAPI";
|
|
||||||
import type { Store, StoreBusinessDay, StoreBusinessDayOpenStatus } from "@/types/Store";
|
|
||||||
import { permissionCodes } from "@/utils/permission";
|
|
||||||
import { getBusinessDayStatusText, getBusinessDayStatusTheme } from "@/utils/store";
|
|
||||||
|
|
||||||
defineOptions({
|
|
||||||
name: "StoreBusinessDayList"
|
|
||||||
});
|
|
||||||
|
|
||||||
const route = useRoute();
|
|
||||||
const storeId = computed(() => String(route.params.id || ""));
|
|
||||||
|
|
||||||
// ---------- 门店信息 ----------
|
|
||||||
|
|
||||||
const store = ref<Store>();
|
|
||||||
const storeName = computed(() => store.value?.name || "门店");
|
|
||||||
const storeCodeText = computed(() => `编码:${store.value?.code || "-"}`);
|
|
||||||
|
|
||||||
// ---------- 列表数据 ----------
|
|
||||||
|
|
||||||
const isRefreshing = ref(false);
|
|
||||||
const isLoading = ref(false);
|
|
||||||
const isNoMore = ref(false);
|
|
||||||
const page = reactive<Page<StoreBusinessDay>>({
|
|
||||||
index: 0,
|
|
||||||
size: 20,
|
|
||||||
equalsExample: {
|
|
||||||
storeId: storeId.value
|
|
||||||
},
|
|
||||||
orderMap: {
|
|
||||||
dateValue: "DESC"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const pageResult = reactive<PageResult<StoreBusinessDay>>({
|
|
||||||
total: 0,
|
|
||||||
list: []
|
|
||||||
});
|
|
||||||
|
|
||||||
async function refresh(): Promise<void> {
|
|
||||||
isRefreshing.value = true;
|
|
||||||
try {
|
|
||||||
page.index = 0;
|
|
||||||
page.equalsExample = {
|
|
||||||
storeId: storeId.value
|
|
||||||
};
|
|
||||||
pageResult.total = 0;
|
|
||||||
pageResult.list = [];
|
|
||||||
isNoMore.value = false;
|
|
||||||
await fetch();
|
|
||||||
} finally {
|
|
||||||
isRefreshing.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetch(): Promise<void> {
|
|
||||||
if (isLoading.value || isNoMore.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoading.value = true;
|
|
||||||
try {
|
|
||||||
const result = await StoreAPI.businessDayList(page);
|
|
||||||
pageResult.total = result.total || 0;
|
|
||||||
pageResult.list = pageResult.list.concat(result.list || []);
|
|
||||||
page.index += 1;
|
|
||||||
isNoMore.value = pageResult.list.length >= pageResult.total || (result.list || []).length < page.size;
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- 编辑 ----------
|
|
||||||
|
|
||||||
const editorVisible = ref(false);
|
|
||||||
const datePickerVisible = ref(false);
|
|
||||||
const datePickerDraft = ref("");
|
|
||||||
const isSaving = ref(false);
|
|
||||||
const editingId = ref("");
|
|
||||||
const formModel = reactive({
|
|
||||||
dateText: "",
|
|
||||||
openStatus: "OPEN" as StoreBusinessDayOpenStatus,
|
|
||||||
reason: ""
|
|
||||||
});
|
|
||||||
const savePermissionCode = computed(() => editingId.value ? permissionCodes.store.update : permissionCodes.store.create);
|
|
||||||
|
|
||||||
function openDayEditor(day?: StoreBusinessDay): void {
|
|
||||||
editingId.value = day?.id || "";
|
|
||||||
Object.assign(formModel, {
|
|
||||||
dateText: day?.dateValue ? formatDateValue(day.dateValue) : formatDateValue(toDateValue(new Date())),
|
|
||||||
openStatus: day?.openStatus || "OPEN",
|
|
||||||
reason: day?.reason || ""
|
|
||||||
});
|
|
||||||
editorVisible.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function openDatePicker(): void {
|
|
||||||
datePickerDraft.value = formModel.dateText;
|
|
||||||
datePickerVisible.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function confirmDatePicker(): void {
|
|
||||||
formModel.dateText = datePickerDraft.value;
|
|
||||||
datePickerVisible.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveDay(): Promise<void> {
|
|
||||||
if (!validateDay()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isSaving.value = true;
|
|
||||||
try {
|
|
||||||
const day: StoreBusinessDay = {
|
|
||||||
id: editingId.value || undefined,
|
|
||||||
storeId: storeId.value,
|
|
||||||
dateValue: parseDateValue(formModel.dateText),
|
|
||||||
openStatus: formModel.openStatus,
|
|
||||||
reason: formModel.reason || undefined
|
|
||||||
};
|
|
||||||
if (editingId.value) {
|
|
||||||
await StoreAPI.updateBusinessDay(day);
|
|
||||||
} else {
|
|
||||||
await StoreAPI.createBusinessDay(day);
|
|
||||||
}
|
|
||||||
Toast.success("营业日已保存");
|
|
||||||
editorVisible.value = false;
|
|
||||||
await refresh();
|
|
||||||
} finally {
|
|
||||||
isSaving.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateDay(): boolean {
|
|
||||||
if (!formModel.dateText) {
|
|
||||||
Toast.warning("请选择日期");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- 删除 ----------
|
|
||||||
|
|
||||||
async function confirmRemoveDay(day: StoreBusinessDay): Promise<void> {
|
|
||||||
if (!day.id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const dayId = day.id;
|
|
||||||
const dialog = DialogPlugin.confirm({
|
|
||||||
title: "删除营业日",
|
|
||||||
content: `确定删除 ${formatDateValue(day.dateValue)} 的营业日配置吗?已有登记记录的营业日后端会拒绝删除。`,
|
|
||||||
cancelBtn: "取消",
|
|
||||||
confirmBtn: "删除",
|
|
||||||
onCancel() {
|
|
||||||
dialog.destroy();
|
|
||||||
},
|
|
||||||
async onConfirm() {
|
|
||||||
dialog.destroy();
|
|
||||||
await removeDay(dayId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function removeDay(id: string): Promise<void> {
|
|
||||||
await StoreAPI.removeBusinessDay(id);
|
|
||||||
Toast.success("营业日已删除");
|
|
||||||
await refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- 日期 ----------
|
|
||||||
|
|
||||||
function formatDateValue(value?: number): string {
|
|
||||||
if (!value) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const text = String(value);
|
|
||||||
return `${text.slice(0, 4)}-${text.slice(4, 6)}-${text.slice(6, 8)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseDateValue(text: string): number {
|
|
||||||
return Number(text.replaceAll("-", ""));
|
|
||||||
}
|
|
||||||
|
|
||||||
function toDateValue(date: Date): number {
|
|
||||||
const year = date.getFullYear();
|
|
||||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
||||||
const day = String(date.getDate()).padStart(2, "0");
|
|
||||||
return Number(`${year}${month}${day}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
try {
|
|
||||||
store.value = await StoreAPI.detail(storeId.value);
|
|
||||||
await fetch();
|
|
||||||
} catch {
|
|
||||||
Toast.error("营业日数据加载失败");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="less">
|
|
||||||
.business-day-list {
|
|
||||||
gap: .75rem;
|
|
||||||
display: grid;
|
|
||||||
padding: calc(var(--app-nav-height) + 1rem) 1rem 6rem 1rem;
|
|
||||||
|
|
||||||
.summary {
|
|
||||||
display: flex;
|
|
||||||
padding: .75rem;
|
|
||||||
align-items: center;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: .75rem;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
div {
|
|
||||||
gap: .25rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
color: #52607a;
|
|
||||||
font-size: .8125rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.item {
|
|
||||||
gap: .5rem;
|
|
||||||
display: flex;
|
|
||||||
padding: .75rem;
|
|
||||||
align-items: center;
|
|
||||||
border: 1px solid var(--td-border-level-1-color);
|
|
||||||
background: #fff;
|
|
||||||
border-radius: .75rem;
|
|
||||||
|
|
||||||
.content {
|
|
||||||
gap: .375rem;
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
min-width: 0;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.date {
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta {
|
|
||||||
color: #52607a;
|
|
||||||
font-size: .875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.delete {
|
|
||||||
flex: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.add {
|
|
||||||
left: 1rem;
|
|
||||||
right: 1rem;
|
|
||||||
bottom: calc(.75rem + env(safe-area-inset-bottom, 0));
|
|
||||||
position: fixed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor {
|
|
||||||
gap: .75rem;
|
|
||||||
display: flex;
|
|
||||||
padding: 1rem;
|
|
||||||
background: #fff;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hint {
|
|
||||||
margin: 0;
|
|
||||||
color: #52607a;
|
|
||||||
font-size: .8125rem;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.picker-btn {
|
|
||||||
gap: .5rem;
|
|
||||||
width: 100%;
|
|
||||||
border: 0;
|
|
||||||
display: flex;
|
|
||||||
padding: 0;
|
|
||||||
color: inherit;
|
|
||||||
background: transparent;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
font: inherit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(html.theme-dark) {
|
|
||||||
.business-day-list {
|
|
||||||
.summary,
|
|
||||||
.item,
|
|
||||||
.editor {
|
|
||||||
background: #1e293b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary span,
|
|
||||||
.item .meta,
|
|
||||||
.editor .hint {
|
|
||||||
color: #94a3b8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.date,
|
|
||||||
.summary strong,
|
|
||||||
.editor h3 {
|
|
||||||
color: #e2e8f0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
@@ -7,11 +7,8 @@
|
|||||||
<t-form-item label="名称">
|
<t-form-item label="名称">
|
||||||
<t-input v-model.trim="formModel.name" placeholder="门店名称" borderless />
|
<t-input v-model.trim="formModel.name" placeholder="门店名称" borderless />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label="状态">
|
<t-form-item label="启用" content-align="right">
|
||||||
<button class="picker-btn" type="button" @click="openStatusPicker">
|
<t-switch v-model="formModel.status" :custom-value="['ACTIVE', 'INACTIVE']" />
|
||||||
<span v-text="getStoreStatusText(formModel.status)" />
|
|
||||||
<t-icon name="chevron-right" />
|
|
||||||
</button>
|
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label="电话">
|
<t-form-item label="电话">
|
||||||
<t-input v-model.trim="formModel.telephone" type="tel" placeholder="联系电话" borderless />
|
<t-input v-model.trim="formModel.telephone" type="tel" placeholder="联系电话" borderless />
|
||||||
@@ -19,29 +16,29 @@
|
|||||||
<t-form-item label="地址">
|
<t-form-item label="地址">
|
||||||
<t-input v-model.trim="formModel.address" placeholder="门店地址" borderless />
|
<t-input v-model.trim="formModel.address" placeholder="门店地址" borderless />
|
||||||
</t-form-item>
|
</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-form-item label="备注">
|
||||||
<t-textarea v-model.trim="formModel.remark" class="textarea" placeholder="备注" autosize borderless />
|
<t-textarea v-model.trim="formModel.remark" class="textarea" placeholder="备注" autosize borderless />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
</t-form>
|
</t-form>
|
||||||
<div v-if="!isCreateMode" class="links">
|
<t-cell-group v-if="!isCreateMode" theme="card">
|
||||||
<t-cell title="营业日管理" note="配置营业或休息日期" arrow hover @click="router.push(`/management/stores/${routeId}/business-days`)" />
|
<t-cell title="营业日管理" note="配置营业或休息日期" arrow hover @click="router.push(`/management/stores/${routeId}/business-days`)" />
|
||||||
</div>
|
</t-cell-group>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<t-button v-if="!isCreateMode" v-can="permissionCodes.store.delete" theme="danger" :loading="isRemoving" @click="confirmRemoveStore">删除</t-button>
|
<t-button
|
||||||
<t-button theme="primary" :loading="isSaving" @click="saveStore">保存</t-button>
|
v-if="!isCreateMode"
|
||||||
|
v-can="permissionCodes.store.delete"
|
||||||
|
class="action"
|
||||||
|
theme="danger"
|
||||||
|
:loading="isRemoving"
|
||||||
|
@click="confirmRemoveStore"
|
||||||
|
>删除</t-button>
|
||||||
|
<t-button
|
||||||
|
class="action"
|
||||||
|
theme="primary"
|
||||||
|
:loading="isSaving"
|
||||||
|
@click="saveStore"
|
||||||
|
>保存</t-button>
|
||||||
</div>
|
</div>
|
||||||
<t-popup v-model="statusPickerVisible" placement="bottom">
|
|
||||||
<t-picker
|
|
||||||
v-model="statusPickerDraft"
|
|
||||||
:columns="statusPickerColumns"
|
|
||||||
title="门店状态"
|
|
||||||
@cancel="statusPickerVisible = false"
|
|
||||||
@confirm="confirmStatusPicker"
|
|
||||||
/>
|
|
||||||
</t-popup>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -50,9 +47,6 @@ import { DialogPlugin, Toast } from "tdesign-mobile-vue";
|
|||||||
import StoreAPI from "@/api/StoreAPI";
|
import StoreAPI from "@/api/StoreAPI";
|
||||||
import type { Store, StoreStatus } from "@/types/Store";
|
import type { Store, StoreStatus } from "@/types/Store";
|
||||||
import { permissionCodes } from "@/utils/permission";
|
import { permissionCodes } from "@/utils/permission";
|
||||||
import { getStoreStatusText } from "@/utils/store";
|
|
||||||
|
|
||||||
type PickerValue = string | number;
|
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "StoreEdit"
|
name: "StoreEdit"
|
||||||
@@ -78,31 +72,9 @@ const formModel = reactive({
|
|||||||
status: "ACTIVE" as StoreStatus,
|
status: "ACTIVE" as StoreStatus,
|
||||||
telephone: "",
|
telephone: "",
|
||||||
address: "",
|
address: "",
|
||||||
remark: "",
|
remark: ""
|
||||||
sortText: "0"
|
|
||||||
});
|
});
|
||||||
|
const sortValue = ref(0);
|
||||||
// ---------- 状态选择 ----------
|
|
||||||
|
|
||||||
const statusPickerVisible = ref(false);
|
|
||||||
const statusPickerDraft = ref<PickerValue[]>(["ACTIVE"]);
|
|
||||||
const statusPickerColumns = [
|
|
||||||
[
|
|
||||||
{ label: "启用", value: "ACTIVE" },
|
|
||||||
{ label: "停用", value: "INACTIVE" }
|
|
||||||
]
|
|
||||||
];
|
|
||||||
|
|
||||||
function openStatusPicker(): void {
|
|
||||||
statusPickerDraft.value = [formModel.status];
|
|
||||||
statusPickerVisible.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function confirmStatusPicker(): void {
|
|
||||||
const nextStatus = String(statusPickerDraft.value[0] || "ACTIVE") as StoreStatus;
|
|
||||||
formModel.status = nextStatus;
|
|
||||||
statusPickerVisible.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- 页面数据 ----------
|
// ---------- 页面数据 ----------
|
||||||
|
|
||||||
@@ -119,9 +91,9 @@ async function fillForm(): Promise<void> {
|
|||||||
status: current.status || "ACTIVE",
|
status: current.status || "ACTIVE",
|
||||||
telephone: current.telephone || "",
|
telephone: current.telephone || "",
|
||||||
address: current.address || "",
|
address: current.address || "",
|
||||||
remark: current.remark || "",
|
remark: current.remark || ""
|
||||||
sortText: String(current.sort ?? 0)
|
|
||||||
});
|
});
|
||||||
|
sortValue.value = current.sort ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 保存 ----------
|
// ---------- 保存 ----------
|
||||||
@@ -141,14 +113,15 @@ async function saveStore(): Promise<void> {
|
|||||||
telephone: formModel.telephone || undefined,
|
telephone: formModel.telephone || undefined,
|
||||||
address: formModel.address || undefined,
|
address: formModel.address || undefined,
|
||||||
remark: formModel.remark || undefined,
|
remark: formModel.remark || undefined,
|
||||||
sort: Number(formModel.sortText || 0)
|
sort: sortValue.value
|
||||||
};
|
};
|
||||||
const savedId = isCreateMode.value ? await StoreAPI.create(store) : formModel.id;
|
if (isCreateMode.value) {
|
||||||
if (!isCreateMode.value) {
|
await StoreAPI.create(store);
|
||||||
|
} else {
|
||||||
await StoreAPI.update(store);
|
await StoreAPI.update(store);
|
||||||
}
|
}
|
||||||
Toast.success("门店已保存");
|
Toast.success("门店已保存");
|
||||||
await router.replace(`/management/stores/${savedId}`);
|
await router.replace("/management/stores");
|
||||||
} finally {
|
} finally {
|
||||||
isSaving.value = false;
|
isSaving.value = false;
|
||||||
}
|
}
|
||||||
@@ -163,10 +136,6 @@ function validateForm(): boolean {
|
|||||||
Toast.warning("请填写门店名称");
|
Toast.warning("请填写门店名称");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (Number.isNaN(Number(formModel.sortText || 0))) {
|
|
||||||
Toast.warning("排序值必须是数字");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,7 +185,10 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
.store-edit {
|
.store-edit {
|
||||||
|
gap: 1rem;
|
||||||
padding: calc(var(--app-nav-height) + 1rem) 0 6rem 0;
|
padding: calc(var(--app-nav-height) + 1rem) 0 6rem 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
.form {
|
.form {
|
||||||
border-radius: .75rem;
|
border-radius: .75rem;
|
||||||
@@ -226,19 +198,6 @@ onMounted(async () => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.picker-btn {
|
|
||||||
gap: .5rem;
|
|
||||||
width: 100%;
|
|
||||||
border: 0;
|
|
||||||
display: flex;
|
|
||||||
padding: 0;
|
|
||||||
color: inherit;
|
|
||||||
background: transparent;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
font: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.links {
|
.links {
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
}
|
}
|
||||||
@@ -248,10 +207,12 @@ onMounted(async () => {
|
|||||||
left: 1rem;
|
left: 1rem;
|
||||||
right: 1rem;
|
right: 1rem;
|
||||||
bottom: calc(.75rem + env(safe-area-inset-bottom, 0));
|
bottom: calc(.75rem + env(safe-area-inset-bottom, 0));
|
||||||
display: grid;
|
display: flex;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
|
.action {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -6,20 +6,30 @@
|
|||||||
@refresh="refresh"
|
@refresh="refresh"
|
||||||
@scrolltolower="fetch"
|
@scrolltolower="fetch"
|
||||||
>
|
>
|
||||||
<div v-for="item in pageResult.list" :key="item.id" class="item" @click="toEdit(item)">
|
<t-cell-group theme="card">
|
||||||
<div class="content">
|
<t-cell
|
||||||
<div class="header">
|
class="item"
|
||||||
<span class="name" v-text="item.name || '未命名门店'" />
|
v-for="item in pageResult.list"
|
||||||
<t-tag size="small" :theme="getStoreStatusTheme(item.status)" variant="outline" :content="getStoreStatusText(item.status)" />
|
:key="item.id"
|
||||||
</div>
|
arrow
|
||||||
<span class="meta" v-text="`编码:${item.code || '-'}`" />
|
@click="toEdit(item)"
|
||||||
<span class="meta" v-text="item.telephone ? `电话:${item.telephone}` : '电话:-'" />
|
>
|
||||||
<span class="meta" v-text="item.address ? `地址:${item.address}` : '地址:-'" />
|
<template #title>
|
||||||
</div>
|
<div class="header">
|
||||||
<t-icon name="chevron-right" size="1.25rem" />
|
<div class="text-bold" v-text="item.name || '未命名门店'"></div>
|
||||||
</div>
|
<t-tag size="small" :theme="getStoreStatusTheme(item.status)" variant="outline" :content="getStoreStatusText(item.status)" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #description>
|
||||||
|
<div class="description">
|
||||||
|
<span class="gray" v-text="item.telephone ? `电话:${item.telephone}` : '电话:-'" />
|
||||||
|
<span class="gray" v-text="item.address ? `地址:${item.address}` : '地址:-'" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</t-cell>
|
||||||
|
</t-cell-group>
|
||||||
<div v-if="!pageResult.list.length" class="empty-block" v-text="isLoading ? '正在加载门店' : '暂无门店'" />
|
<div v-if="!pageResult.list.length" class="empty-block" v-text="isLoading ? '正在加载门店' : '暂无门店'" />
|
||||||
<div v-if="pageResult.list.length" class="load-state" v-text="isNoMore ? '已经到底了' : '上拉加载更多'" />
|
<div v-if="pageResult.list.length" class="load-state" v-text="isNoMore ? '已加载全部门店' : '上拉加载更多'" />
|
||||||
</t-pull-down-refresh>
|
</t-pull-down-refresh>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<t-button v-can="permissionCodes.store.export" theme="default" :loading="isExporting" @click="exportExcel">
|
<t-button v-can="permissionCodes.store.export" theme="default" :loading="isExporting" @click="exportExcel">
|
||||||
@@ -40,8 +50,9 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Toast } from "tdesign-mobile-vue";
|
import { Toast } from "tdesign-mobile-vue";
|
||||||
import { Time, Toolkit, type Page, type PageResult } from "timi-web";
|
import { Time, Toolkit, type Page, type PageResult, useUserStore } from "timi-web";
|
||||||
import StoreAPI from "@/api/StoreAPI";
|
import StoreAPI from "@/api/StoreAPI";
|
||||||
|
import { useNavBarStore } from "@/store/navBarStore";
|
||||||
import type { Store } from "@/types/Store";
|
import type { Store } from "@/types/Store";
|
||||||
import { permissionCodes } from "@/utils/permission";
|
import { permissionCodes } from "@/utils/permission";
|
||||||
import { getStoreStatusText, getStoreStatusTheme } from "@/utils/store";
|
import { getStoreStatusText, getStoreStatusTheme } from "@/utils/store";
|
||||||
@@ -51,6 +62,8 @@ defineOptions({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const navBarStore = useNavBarStore();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
// ---------- 列表数据 ----------
|
// ---------- 列表数据 ----------
|
||||||
|
|
||||||
@@ -132,52 +145,41 @@ async function toEdit(item: Store): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
navBarStore.clearRight();
|
||||||
|
if (userStore.hasPermissions(permissionCodes.store.update)) {
|
||||||
|
navBarStore.setRightText("排序", "var(--td-brand-color)", () => {
|
||||||
|
router.push("/management/stores/sort");
|
||||||
|
});
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await fetch();
|
await fetch();
|
||||||
} catch {
|
} catch {
|
||||||
Toast.error("门店列表加载失败");
|
Toast.error("门店列表加载失败");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
navBarStore.clearRight();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
.store-list {
|
.store-list {
|
||||||
gap: .75rem;
|
gap: .75rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
padding: calc(var(--app-nav-height) + 1rem) 1rem 6rem 1rem;
|
padding: calc(var(--app-nav-height) + 1rem) 0 6rem 0;
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
gap: .75rem;
|
|
||||||
display: flex;
|
|
||||||
padding: .75rem;
|
|
||||||
align-items: center;
|
|
||||||
border: 1px solid var(--td-border-level-1-color);
|
|
||||||
background: #fff;
|
|
||||||
border-radius: .75rem;
|
|
||||||
|
|
||||||
.content {
|
|
||||||
gap: .375rem;
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
min-width: 0;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
gap: .5rem;
|
gap: .5rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.name {
|
.description {
|
||||||
font-size: 1rem;
|
display: flex;
|
||||||
font-weight: 700;
|
flex-direction: column;
|
||||||
}
|
|
||||||
|
|
||||||
.meta {
|
|
||||||
color: #52607a;
|
|
||||||
font-size: .875rem;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,4 +210,3 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,327 @@
|
|||||||
|
<template>
|
||||||
|
<div v-can="permissionCodes.store.update" class="store-sort">
|
||||||
|
<div class="tip" v-text="'按住右侧拖动手柄调整门店顺序,保存后生效'" />
|
||||||
|
<div ref="sortListRef" class="items">
|
||||||
|
<t-cell-group v-if="storeList.length" theme="card">
|
||||||
|
<t-cell
|
||||||
|
v-for="(item, index) in storeList"
|
||||||
|
:key="item.id"
|
||||||
|
class="cell"
|
||||||
|
:title="item.name || '未命名门店'"
|
||||||
|
:description="`编码:${item.code || '-'}`"
|
||||||
|
:class="{ dragging: draggingIndex === index }"
|
||||||
|
>
|
||||||
|
<template #note>
|
||||||
|
<t-tag :theme="getStatusTheme(item.status)" variant="outline" :content="getStatusText(item.status)" />
|
||||||
|
</template>
|
||||||
|
<template #rightIcon>
|
||||||
|
<t-icon
|
||||||
|
class="handle"
|
||||||
|
role="button"
|
||||||
|
name="move"
|
||||||
|
aria-label="拖动排序"
|
||||||
|
@pointerdown.stop="startDrag($event, index)"
|
||||||
|
@pointermove.stop="moveDrag"
|
||||||
|
@pointerup.stop="finishDrag"
|
||||||
|
@pointercancel.stop="finishDrag"
|
||||||
|
@lostpointercapture.stop="finishDrag"
|
||||||
|
@touchstart.stop="startTouchDrag($event, index)"
|
||||||
|
@touchmove.stop="moveTouchDrag"
|
||||||
|
@touchend.stop="finishTouchDrag"
|
||||||
|
@touchcancel.stop="finishTouchDrag"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</t-cell>
|
||||||
|
</t-cell-group>
|
||||||
|
<div v-else class="empty-block" v-text="isLoading ? '正在加载门店' : '暂无门店'" />
|
||||||
|
</div>
|
||||||
|
<t-button
|
||||||
|
class="save"
|
||||||
|
theme="primary"
|
||||||
|
:loading="isSaving"
|
||||||
|
:disabled="!sortDirty || isSaving || isLoading"
|
||||||
|
@click="saveSort"
|
||||||
|
>
|
||||||
|
保存排序
|
||||||
|
</t-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Toast, type TagProps } from "tdesign-mobile-vue";
|
||||||
|
import StoreAPI from "@/api/StoreAPI";
|
||||||
|
import { useNavBarStore } from "@/store/navBarStore";
|
||||||
|
import type { Store, StoreStatus } from "@/types/Store";
|
||||||
|
import { permissionCodes } from "@/utils/permission";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "StoreSort"
|
||||||
|
});
|
||||||
|
|
||||||
|
const navBarStore = useNavBarStore();
|
||||||
|
|
||||||
|
// ---------- 列表数据 ----------
|
||||||
|
|
||||||
|
const storeList = ref<Store[]>([]);
|
||||||
|
const sortListRef = ref<HTMLElement>();
|
||||||
|
const isLoading = ref(false);
|
||||||
|
const isSaving = ref(false);
|
||||||
|
const sortDirty = ref(false);
|
||||||
|
|
||||||
|
// ---------- 拖拽状态 ----------
|
||||||
|
|
||||||
|
const draggingIndex = ref(-1);
|
||||||
|
let pointerId: number | undefined;
|
||||||
|
let pointerStartY = 0;
|
||||||
|
let pointerMoved = false;
|
||||||
|
let pointerTarget: HTMLElement | undefined;
|
||||||
|
let dragInputType: "pointer" | "touch" | undefined;
|
||||||
|
|
||||||
|
// ---------- 展示文案 ----------
|
||||||
|
|
||||||
|
function getStatusText(status?: StoreStatus): string {
|
||||||
|
return status === "ACTIVE" ? "启用" : "停用";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusTheme(status?: StoreStatus): TagProps["theme"] {
|
||||||
|
return status === "ACTIVE" ? "primary" : "default";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 拖拽排序 ----------
|
||||||
|
|
||||||
|
function resolveDropIndex(clientY: number): number {
|
||||||
|
const elements = sortListRef.value?.querySelectorAll<HTMLElement>(".cell");
|
||||||
|
if (!elements?.length) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let index = 0; index < elements.length; index++) {
|
||||||
|
const rect = elements[index].getBoundingClientRect();
|
||||||
|
if (clientY < rect.top + rect.height / 2) {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return elements.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function beginDrag(index: number, clientY: number, target: EventTarget | null): void {
|
||||||
|
pointerStartY = clientY;
|
||||||
|
pointerMoved = false;
|
||||||
|
draggingIndex.value = index;
|
||||||
|
pointerTarget = target instanceof HTMLElement ? target : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveDragAt(clientY: number): void {
|
||||||
|
if (0 > draggingIndex.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pointerMoved && 8 > Math.abs(clientY - pointerStartY)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pointerMoved = true;
|
||||||
|
const targetIndex = resolveDropIndex(clientY);
|
||||||
|
if (0 > targetIndex || targetIndex === draggingIndex.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [item] = storeList.value.splice(draggingIndex.value, 1);
|
||||||
|
storeList.value.splice(targetIndex, 0, item);
|
||||||
|
draggingIndex.value = targetIndex;
|
||||||
|
sortDirty.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishDragState(): void {
|
||||||
|
if (pointerMoved) {
|
||||||
|
sortDirty.value = true;
|
||||||
|
}
|
||||||
|
pointerId = undefined;
|
||||||
|
pointerTarget = undefined;
|
||||||
|
dragInputType = undefined;
|
||||||
|
draggingIndex.value = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function startDrag(event: PointerEvent, index: number): void {
|
||||||
|
if (isSaving.value || isLoading.value || dragInputType || !event.isPrimary || event.button !== 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
pointerId = event.pointerId;
|
||||||
|
dragInputType = "pointer";
|
||||||
|
beginDrag(index, event.clientY, event.currentTarget);
|
||||||
|
pointerTarget?.setPointerCapture(event.pointerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveDrag(event: PointerEvent): void {
|
||||||
|
if (dragInputType !== "pointer" || pointerId !== event.pointerId || 0 > draggingIndex.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
moveDragAt(event.clientY);
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishDrag(event: PointerEvent): void {
|
||||||
|
if (dragInputType !== "pointer" || pointerId !== event.pointerId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pointerTarget?.hasPointerCapture(event.pointerId)) {
|
||||||
|
pointerTarget.releasePointerCapture(event.pointerId);
|
||||||
|
}
|
||||||
|
finishDragState();
|
||||||
|
}
|
||||||
|
|
||||||
|
function startTouchDrag(event: TouchEvent, index: number): void {
|
||||||
|
if (isSaving.value || isLoading.value || dragInputType || event.touches.length !== 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
const touch = event.touches[0];
|
||||||
|
pointerId = touch.identifier;
|
||||||
|
dragInputType = "touch";
|
||||||
|
beginDrag(index, touch.clientY, event.currentTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveTouchDrag(event: TouchEvent): void {
|
||||||
|
if (dragInputType !== "touch" || pointerId === undefined || 0 > draggingIndex.value || event.touches.length !== 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
moveDragAt(event.touches[0].clientY);
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishTouchDrag(event: TouchEvent): void {
|
||||||
|
if (dragInputType !== "touch" || pointerId === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const touch = event.changedTouches[0];
|
||||||
|
if (touch && touch.identifier !== pointerId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
finishDragState();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 保存 ----------
|
||||||
|
|
||||||
|
async function saveSort(): Promise<void> {
|
||||||
|
if (!sortDirty.value || isSaving.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isSaving.value = true;
|
||||||
|
try {
|
||||||
|
const idList = storeList.value
|
||||||
|
.map(item => item.id)
|
||||||
|
.filter((id): id is string => Boolean(id));
|
||||||
|
await StoreAPI.sort(idList);
|
||||||
|
storeList.value.forEach((item, index) => {
|
||||||
|
item.sort = index;
|
||||||
|
});
|
||||||
|
sortDirty.value = false;
|
||||||
|
Toast.success("门店排序已保存");
|
||||||
|
} catch {
|
||||||
|
Toast.error("门店排序保存失败");
|
||||||
|
} finally {
|
||||||
|
isSaving.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 页面数据 ----------
|
||||||
|
|
||||||
|
async function loadData(): Promise<void> {
|
||||||
|
isLoading.value = true;
|
||||||
|
try {
|
||||||
|
const result = await StoreAPI.list({
|
||||||
|
index: 0,
|
||||||
|
size: 100,
|
||||||
|
orderMap: {
|
||||||
|
sort: "ASC",
|
||||||
|
createdAt: "DESC"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
storeList.value = result.list || [];
|
||||||
|
sortDirty.value = false;
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
navBarStore.clearRight();
|
||||||
|
try {
|
||||||
|
await loadData();
|
||||||
|
} catch {
|
||||||
|
Toast.error("门店加载失败");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
navBarStore.clearRight();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.store-sort {
|
||||||
|
padding: calc(var(--app-nav-height) + 1rem) 0 6rem;
|
||||||
|
|
||||||
|
.tip {
|
||||||
|
color: #52607a;
|
||||||
|
padding: 0 1rem .75rem;
|
||||||
|
font-size: .8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items {
|
||||||
|
.cell {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&.dragging {
|
||||||
|
z-index: 1;
|
||||||
|
box-shadow: 0 .5rem 1rem rgba(15, 23, 42, .16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.handle {
|
||||||
|
color: #52607a;
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.dragging,
|
||||||
|
&.dragging * {
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-block {
|
||||||
|
color: #52607a;
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save {
|
||||||
|
left: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
bottom: calc(.75rem + env(safe-area-inset-bottom, 0));
|
||||||
|
position: fixed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html.theme-dark) {
|
||||||
|
.store-sort {
|
||||||
|
.tip,
|
||||||
|
.empty-block,
|
||||||
|
.handle {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell.dragging {
|
||||||
|
box-shadow: 0 .5rem 1rem rgba(2, 6, 23, .28);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -3,9 +3,10 @@ import ManagementSettings from "@/pages/management/ManagementSettings.vue";
|
|||||||
import ManagementProfile from "@/pages/management/ManagementProfile.vue";
|
import ManagementProfile from "@/pages/management/ManagementProfile.vue";
|
||||||
import ManagementPassword from "@/pages/management/ManagementPassword.vue";
|
import ManagementPassword from "@/pages/management/ManagementPassword.vue";
|
||||||
import ManagementQrCode from "@/pages/management/ManagementQrCode.vue";
|
import ManagementQrCode from "@/pages/management/ManagementQrCode.vue";
|
||||||
import StoreBusinessDayList from "@/pages/management/StoreBusinessDayList.vue";
|
import StoreBusinessDayCalendar from "@/pages/management/StoreBusinessDayCalendar.vue";
|
||||||
import StoreEdit from "@/pages/management/StoreEdit.vue";
|
import StoreEdit from "@/pages/management/StoreEdit.vue";
|
||||||
import StoreList from "@/pages/management/StoreList.vue";
|
import StoreList from "@/pages/management/StoreList.vue";
|
||||||
|
import StoreSort from "@/pages/management/StoreSort.vue";
|
||||||
|
|
||||||
export const storeRoutes: RouteRecordRaw[] = [
|
export const storeRoutes: RouteRecordRaw[] = [
|
||||||
{
|
{
|
||||||
@@ -36,6 +37,20 @@ export const storeRoutes: RouteRecordRaw[] = [
|
|||||||
darkBodyBackground: "#10161f"
|
darkBodyBackground: "#10161f"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/management/stores/sort",
|
||||||
|
name: "StoreSort",
|
||||||
|
component: StoreSort,
|
||||||
|
meta: {
|
||||||
|
depth: 4,
|
||||||
|
navBarVisible: true,
|
||||||
|
navBarCanBack: true,
|
||||||
|
navBarTitle: "门店排序",
|
||||||
|
tabBarVisible: false,
|
||||||
|
bodyBackground: "#F5F5F8",
|
||||||
|
darkBodyBackground: "#10161f"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/management/stores/:id",
|
path: "/management/stores/:id",
|
||||||
name: "StoreEdit",
|
name: "StoreEdit",
|
||||||
@@ -52,8 +67,8 @@ export const storeRoutes: RouteRecordRaw[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/management/stores/:id/business-days",
|
path: "/management/stores/:id/business-days",
|
||||||
name: "StoreBusinessDayList",
|
name: "StoreBusinessDayCalendar",
|
||||||
component: StoreBusinessDayList,
|
component: StoreBusinessDayCalendar,
|
||||||
meta: {
|
meta: {
|
||||||
depth: 5,
|
depth: 5,
|
||||||
navBarVisible: true,
|
navBarVisible: true,
|
||||||
|
|||||||
@@ -37,3 +37,10 @@ html.theme-dark body {
|
|||||||
.t-form__label {
|
.t-form__label {
|
||||||
color: #555;
|
color: #555;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.load-state {
|
||||||
|
color: var(--tui-gray);
|
||||||
|
padding: .75rem;
|
||||||
|
text-align: center;
|
||||||
|
font-size: .875rem;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user