add StoreDiary

This commit is contained in:
Timi
2026-08-31 17:32:01 +08:00
parent 95a400cd93
commit 1e83fdf883
9 changed files with 1288 additions and 1 deletions
+268
View File
@@ -0,0 +1,268 @@
<template>
<div v-can="permissionCodes.storeDiary.read" class="store-diary-detail">
<template v-if="article">
<header class="header" v-entrance-transition>
<h1 v-text="article.title || '未命名日记'" />
<div v-if="article.description" class="description" v-text="article.description" />
<pass-time-label
class="time gray"
v-if="article.createdAt"
:timestamp="article.createdAt"
underline
/>
</header>
<t-divider />
<section class="content" v-entrance-transition="70">
<article-view
:article="article"
empty-text="暂无日记正文"
:resolve-attachment-url="resolveAttachmentUrl"
/>
</section>
<footer class="footer gray">
<div class="updater">
<div>最近更新人:</div>
<div v-if="article.updater" class="updater">
<t-image
class="avatar"
:src="getUpdaterAvatar(article.updater)"
:alt="getUpdaterName(article.updater)"
fit="cover"
/>
<span v-text="getUpdaterName(article.updater)" />
</div>
<span v-else v-text="'未知用户'" />
</div>
<div class="updated-at">
<span>最近更新时间:</span>
<pass-time-label v-if="article.updatedAt" :timestamp="article.updatedAt" />
<span v-else v-text="'未知时间'" />
</div>
</footer>
<t-button
v-if="canDelete"
class="delete cell-button"
theme="danger"
block
:loading="isDeleting"
@click="confirmDelete"
>
删除日记
</t-button>
</template>
<t-empty v-else :description="isLoading ? '正在加载门店日记' : '门店日记不存在'" />
</div>
</template>
<script setup lang="ts">
import { ArticleView } from "timi-tdesign-mobile";
import { DialogPlugin } from "tdesign-mobile-vue";
import { PassTimeLabel } from "timi-web";
import { UserAPI, useUserStore } from "timi-web-api";
import StoreDiaryAPI from "@/api/StoreDiaryAPI";
import { useCurrentStoreStore } from "@/store/currentStoreStore";
import { useNavBarStore } from "@/store/navBarStore";
import type { StoreDiary } from "@/types/StoreDiary";
import { getAttachmentReadURL } from "@/utils/attachment";
import { buildFallbackAvatar } from "@/utils/avatar";
import { getErrorMessage } from "@/utils/error";
import { Message } from "@/utils/message";
import { permissionCodes } from "@/utils/permission";
defineOptions({
name: "StoreDiaryDetail"
});
const route = useRoute();
const router = useRouter();
const navBarStore = useNavBarStore();
const userStore = useUserStore();
const currentStoreStore = useCurrentStoreStore();
const canUpdate = computed(() => userStore.hasPermissions(permissionCodes.storeDiary.update));
const canDelete = computed(() => userStore.hasPermissions(permissionCodes.storeDiary.delete));
// ---------- 日记数据 ----------
const article = ref<StoreDiary>();
const isLoading = ref(false);
const isDeleting = ref(false);
let loadToken = 0;
function resolveAttachmentUrl(attachmentId: string): string {
return getAttachmentReadURL(attachmentId);
}
function getUpdaterName(item: NonNullable<StoreDiary["updater"]>): string {
return item.nick?.trim() || item.name?.trim() || "未知用户";
}
function getUpdaterAvatar(item: NonNullable<StoreDiary["updater"]>): string {
return UserAPI.getAvatarURL(item) || buildFallbackAvatar(getUpdaterName(item));
}
async function prepareStoreScope(): Promise<void> {
await currentStoreStore.loadLoginStore();
}
function setRightAction(): void {
if (article.value?.id && canUpdate.value) {
navBarStore.setRightText("编辑", "var(--td-brand-color)", toEdit);
return;
}
navBarStore.clearRight();
}
async function loadArticle(): Promise<void> {
const requestToken = loadToken + 1;
loadToken = requestToken;
article.value = undefined;
navBarStore.clearRight();
const id = String(route.params.id || "");
if (!id) {
return;
}
isLoading.value = true;
try {
await prepareStoreScope();
const result = await StoreDiaryAPI.detail(id);
if (requestToken !== loadToken) {
return;
}
article.value = result;
setRightAction();
} catch (error) {
Message.error(getErrorMessage(error, "门店日记加载失败"));
} finally {
if (requestToken === loadToken) {
isLoading.value = false;
}
}
}
// ---------- 操作 ----------
async function toEdit(): Promise<void> {
if (!article.value?.id || !canUpdate.value) {
return;
}
await router.push({
name: "StoreDiaryEdit",
params: { id: article.value.id }
});
}
async function confirmDelete(): Promise<void> {
if (!article.value?.id || !canDelete.value || isDeleting.value) {
return;
}
const diaryId = article.value.id;
const dialog = DialogPlugin.confirm({
title: "删除门店日记",
content: `确定删除“${article.value.title || "未命名日记"}”吗?删除后无法恢复。`,
cancelBtn: "取消",
confirmBtn: "删除",
onCancel() {
dialog.destroy();
},
async onConfirm() {
dialog.destroy();
isDeleting.value = true;
navBarStore.clearRight();
try {
await StoreDiaryAPI.remove(diaryId);
Message.success("门店日记已删除");
await router.replace({ name: "StoreDiaryList" });
} catch (error) {
Message.error(getErrorMessage(error, "门店日记删除失败"));
setRightAction();
} finally {
isDeleting.value = false;
}
}
});
}
onMounted(loadArticle);
onUnmounted(() => navBarStore.clearRight());
</script>
<style scoped lang="less">
.store-diary-detail {
padding: var(--app-nav-height) 0 calc(1rem + env(safe-area-inset-bottom, 0));
.header {
padding: 1.25rem 1rem;
h1 {
margin: 0;
font-size: 1.35rem;
text-align: center;
line-height: 1.5;
word-break: break-word;
}
.time {
display: block;
margin-top: 1rem;
font-size: .875rem;
text-align: center;
}
.description {
color: var(--td-text-color-secondary);
font-size: .875rem;
margin-top: .75rem;
word-break: break-word;
line-height: 1.6;
white-space: pre-wrap;
}
}
.content {
padding: 1rem;
:deep(.content) {
border: none;
padding: 0;
}
}
.footer {
padding: 0 1rem;
font-size: .875rem;
.updater {
gap: .5rem;
display: flex;
align-items: center;
justify-content: center;
.avatar {
width: 1.5rem;
height: 1.5rem;
border-radius: 50%;
}
}
.updated-at {
text-align: center;
}
}
.delete {
margin-top: 3rem;
}
}
:global(html.theme-dark) {
.store-diary-detail {
.meta,
.description {
color: #94a3b8;
}
}
}
</style>
+322
View File
@@ -0,0 +1,322 @@
<template>
<div v-can="pageAccess" class="store-diary-edit">
<t-form class="form" label-align="left" label-width="5rem" v-entrance-transition>
<t-form-item label="所属门店">
<div class="picker-text" v-text="selectedStoreName" />
</t-form-item>
<t-form-item label="日记标题" required-mark>
<t-input
v-model.trim="article.title"
borderless
maxlength="100"
placeholder="请输入日记标题"
/>
</t-form-item>
<t-form-item label="日记摘要">
<t-textarea
v-model.trim="article.description"
borderless
maxlength="200"
autosize
placeholder="可填写本篇日记摘要"
/>
</t-form-item>
<section class="editor-wrapper" v-entrance-transition="70">
<article-editor
ref="articleEditorRef"
:model-value="article"
class="editor"
placeholder="请输入门店日记正文,可插入图片或视频"
:upload-image="uploadImage"
:upload-video="uploadVideo"
:resolve-attachment-url="resolveAttachmentUrl"
@update:model-value="updateArticle"
/>
</section>
</t-form>
</div>
</template>
<script setup lang="ts">
import {
ArticleEditor,
type Article as ArticleEditorArticle,
type ArticleEditorExpose,
type UploadResult
} from "timi-tdesign-mobile";
import { AttachmentAPI, useUserStore } from "timi-web-api";
import StoreDiaryAPI from "@/api/StoreDiaryAPI";
import { useCurrentStoreStore } from "@/store/currentStoreStore";
import { useNavBarStore } from "@/store/navBarStore";
import type { StoreDiary } from "@/types/StoreDiary";
import { getErrorMessage } from "@/utils/error";
import { compressUploadImage } from "@/utils/imageCompression";
import { getAttachmentReadURL } from "@/utils/attachment";
import { Message } from "@/utils/message";
import { permissionCodes } from "@/utils/permission";
defineOptions({
name: "StoreDiaryEdit"
});
const route = useRoute();
const router = useRouter();
const navBarStore = useNavBarStore();
const userStore = useUserStore();
const currentStoreStore = useCurrentStoreStore();
const routeId = computed(() => String(route.params.id || ""));
const isCreateMode = computed(() => !routeId.value);
const savePermissionCode = computed(() => isCreateMode.value ? permissionCodes.storeDiary.create : permissionCodes.storeDiary.update);
const pageAccess = computed(() => ({
value: [permissionCodes.storeDiary.read, savePermissionCode.value],
mode: "and" as const
}));
// ---------- 日记表单 ----------
const isSaving = ref(false);
const articleEditorRef = ref<ArticleEditorExpose>();
const article = ref<StoreDiary>(createEmptyArticle());
function createEmptyArticle(): StoreDiary {
return {
bizType: "GAO_STORE_DIARY",
contentType: "TIP_TAP",
content: "",
title: "",
description: "",
attachmentList: [],
status: "NORMAL"
};
}
function normalizeContentType(value?: string): StoreDiary["contentType"] {
if (value === "TEXT" || value === "MARKDOWN" || value === "TIP_TAP") {
return value;
}
return "TIP_TAP";
}
function updateArticle(value: ArticleEditorArticle): void {
article.value = {
...article.value,
...value,
contentType: normalizeContentType(value.contentType)
};
}
const selectedStoreName = computed(() => {
const store = currentStoreStore.storeList.find(item => item.id === article.value.bizId)
|| (currentStoreStore.loginStore?.id === article.value.bizId ? currentStoreStore.loginStore : undefined);
return store?.name || store?.code || "请选择门店";
});
// ---------- 媒体上传 ----------
async function uploadMedia(file: File): Promise<UploadResult> {
const [tempFile] = await AttachmentAPI.upload(file);
if (!tempFile?.id) {
throw new Error("媒体上传失败");
}
return {
id: tempFile.id,
url: AttachmentAPI.getTempReadURL(tempFile.id),
name: file.name
};
}
async function uploadImage(file: File): Promise<UploadResult> {
return await uploadMedia(await compressUploadImage(file));
}
async function uploadVideo(file: File): Promise<UploadResult> {
return await uploadMedia(file);
}
function resolveAttachmentUrl(attachmentId: string): string {
return getAttachmentReadURL(attachmentId);
}
// ---------- 保存 ----------
function getContentText(content: string): string {
const parser = new DOMParser();
return parser.parseFromString(content, "text/html").body.textContent?.trim() || "";
}
function hasMediaContent(content: string): boolean {
const parser = new DOMParser();
return Boolean(parser.parseFromString(content, "text/html").querySelector("img, video"));
}
function validateArticle(value: StoreDiary): boolean {
if (!value.bizId) {
Message.warning("请选择门店");
return false;
}
if (!value.title?.trim()) {
Message.warning("请输入日记标题");
return false;
}
const content = value.content || "";
if (!getContentText(content) && !hasMediaContent(content)) {
Message.warning("请输入日记正文");
return false;
}
return true;
}
async function save(): Promise<void> {
if (isSaving.value) {
return;
}
if (!article.value.title?.trim()) {
Message.warning("请输入日记标题");
return;
}
isSaving.value = true;
try {
const editorValue = await articleEditorRef.value?.submit();
const value: StoreDiary = {
...article.value,
...(editorValue || {}),
title: article.value.title?.trim() || "",
description: article.value.description?.trim() || "",
contentType: normalizeContentType(editorValue?.contentType ?? article.value.contentType),
content: editorValue?.content ?? article.value.content ?? "",
attachmentList: editorValue?.attachmentList ?? article.value.attachmentList ?? [],
status: article.value.status || "NORMAL"
};
if (!validateArticle(value)) {
return;
}
article.value = value;
if (isCreateMode.value) {
const id = await StoreDiaryAPI.create(value);
Message.success("门店日记已创建");
await router.replace({
name: "StoreDiaryDetail",
params: { id }
});
return;
}
await StoreDiaryAPI.update(value);
Message.success("门店日记已保存");
await router.replace({
name: "StoreDiaryDetail",
params: { id: routeId.value }
});
} catch (error) {
Message.error(getErrorMessage(error, "门店日记保存失败"));
} finally {
isSaving.value = false;
}
}
// ---------- 页面数据 ----------
async function prepareStoreScope(): Promise<void> {
const store = await currentStoreStore.loadLoginStore();
if (store?.id) {
article.value.bizId = store.id;
}
}
async function loadArticle(): Promise<void> {
await prepareStoreScope();
if (isCreateMode.value) {
article.value = {
...createEmptyArticle(),
bizId: article.value.bizId || currentStoreStore.currentStoreId
};
return;
}
const result = await StoreDiaryAPI.detail(routeId.value);
article.value = {
...result,
title: result.title || "",
description: result.description || "",
contentType: result.contentType || "TIP_TAP",
content: result.content || "",
attachmentList: result.attachmentList || []
};
}
onMounted(async () => {
if (userStore.hasPermissions(savePermissionCode.value)) {
navBarStore.setRightText("保存", "var(--td-brand-color)", save);
}
try {
await loadArticle();
} catch (error) {
Message.error(getErrorMessage(error, "门店日记加载失败"));
}
});
onUnmounted(() => navBarStore.clearRight());
</script>
<style scoped lang="less">
.store-diary-edit {
min-height: 100%;
padding-top: var(--app-nav-height);
.form {
margin: 0;
border-radius: 0;
.picker-text {
width: 100%;
text-align: right;
}
.editor-wrapper {
width: 100%;
min-width: 0;
max-width: 100%;
overflow: visible;
.editor {
width: 100%;
min-width: 0;
max-width: 100%;
border: none;
border-radius: 0;
}
:deep(.tp-editor) {
width: 100%;
min-width: 0;
max-width: 100%;
height: auto;
overflow: visible;
}
:deep(.content) {
overflow: visible;
}
:deep(.toolbar) {
width: 100%;
min-width: 0;
max-width: 100%;
top: var(--app-nav-height);
z-index: 10;
position: sticky;
}
:deep(.toolbar .scroll) {
width: 100%;
min-width: 0;
max-width: 100%;
}
}
}
}
</style>
+505
View File
@@ -0,0 +1,505 @@
<template>
<div v-can="permissionCodes.storeDiary.read" class="store-diary-list">
<t-pull-down-refresh
class="list-wrapper"
v-model="isRefreshing"
:refresh-timeout="30000"
@refresh="refresh"
@scrolltolower="loadMore"
>
<section class="calendar" v-entrance-transition>
<t-calendar
:key="calendarRenderKey"
class="calendar-view"
:value="selectedCalendarDate"
:format="calendarFormat"
switch-mode="month"
:use-popup="false"
@panel-change="handleCalendarPanelChange"
@select="selectCalendarDate"
/>
</section>
<t-cell-group v-if="pageResult.list.length" class="items fix-last-cell-line" theme="card">
<t-swipe-cell
v-for="(item, index) in pageResult.list"
:key="item.id || index"
class="item"
v-entrance-transition="index * 30"
>
<t-cell
class="cell"
:title="getTitle(item)"
:description="getDescription(item)"
arrow
@click="toDetail(item)"
>
<template #note>
<pass-time-label :timestamp="item.updatedAt || item.createdAt" underline />
</template>
</t-cell>
<template v-if="canDelete" #right>
<t-button
class="swipe-cell-button"
theme="danger"
:loading="isDeleting && deletingId === item.id"
@click.stop="confirmDelete(item)"
>
删除
</t-button>
</template>
</t-swipe-cell>
</t-cell-group>
<div class="load-state" v-text="loadStateText" />
</t-pull-down-refresh>
</div>
<nav-bar-action-menu v-if="canShowActionMenu" :actions="actionList" />
<bottom-popup
v-model="diaryPopupVisible"
:title="`${selectedDateText} 门店日记`"
height="min(80vh, 40rem)"
left-button-text="关闭"
right-button-text=""
@left-button-click="closeDiaryPopup"
>
<div class="diary-popup">
<t-cell-group v-if="selectedDateDiaries.length" theme="card">
<t-cell
v-for="(item, index) in selectedDateDiaries"
:key="item.id || index"
class="cell"
:title="getTitle(item)"
:description="getDescription(item)"
arrow
hover
@click="toSelectedDiary(item)"
>
<template #note>
<pass-time-label :timestamp="item.updatedAt || item.createdAt" underline />
</template>
</t-cell>
</t-cell-group>
<div v-else class="popup-empty" v-text="'该日期暂无门店日记'" />
</div>
</bottom-popup>
</template>
<script setup lang="ts">
import { BottomPopup } from "timi-tdesign-mobile";
import { type CalendarProps, DialogPlugin, type TDate } from "tdesign-mobile-vue";
import { type PageResult, useUserStore } from "timi-web-api";
import { PassTimeLabel, Time } from "timi-web";
import StoreDiaryAPI, { type StoreDiaryListOptions } from "@/api/StoreDiaryAPI";
import { useCurrentStoreStore } from "@/store/currentStoreStore";
import { useNavBarStore } from "@/store/navBarStore";
import type { NavBarAction } from "@/types/NavBarAction";
import type { StoreDiary } from "@/types/StoreDiary";
import { getErrorMessage } from "@/utils/error";
import { Message } from "@/utils/message";
import { permissionCodes } from "@/utils/permission";
defineOptions({
name: "StoreDiaryList"
});
const router = useRouter();
const navBarStore = useNavBarStore();
const userStore = useUserStore();
const currentStoreStore = useCurrentStoreStore();
const canCreate = computed(() => userStore.hasPermissions(permissionCodes.storeDiary.create));
const canDelete = computed(() => userStore.hasPermissions(permissionCodes.storeDiary.delete));
const canShowActionMenu = computed(() => canCreate.value);
// ---------- 日历 ----------
const today = new Date();
const selectedCalendarDate = ref<Date>(today);
const calendarMonth = reactive({
year: today.getFullYear(),
month: today.getMonth() + 1
});
const calendarRenderKey = ref(0);
const calendarDiaryList = ref<StoreDiary[]>([]);
const isCalendarLoading = ref(false);
const diaryPopupVisible = ref(false);
const selectedDateText = ref("");
const selectedDateDiaries = ref<StoreDiary[]>([]);
let calendarLoadToken = 0;
const diaryDateMap = computed(() => {
const map = new Map<string, StoreDiary[]>();
for (const item of calendarDiaryList.value) {
const dateText = getDiaryDateText(item);
if (!dateText) {
continue;
}
const diaries = map.get(dateText) || [];
diaries.push(item);
map.set(dateText, diaries);
}
for (const diaries of map.values()) {
diaries.sort((left, right) => getDiaryTimestamp(right) - getDiaryTimestamp(left));
}
return map;
});
const calendarFormat = computed<NonNullable<CalendarProps["format"]>>(() => {
const dateMap = diaryDateMap.value;
return (dateItem: TDate) => {
const diaries = dateMap.get(Time.formatDate(dateItem.date)) || [];
if (diaries.length < 1) {
return dateItem;
}
return {
...dateItem,
className: "has-diary",
suffix: String(diaries.length)
};
};
});
// ---------- 顶部操作 ----------
const actionList = computed<NavBarAction[]>(() => {
if (!canCreate.value) {
return [];
}
return [{
action: toCreate,
theme: "primary",
icon: "add",
key: "create",
text: "新增日记"
}];
});
// ---------- 列表数据 ----------
const page = reactive({
index: 0,
size: 20
});
const pageResult = reactive<PageResult<StoreDiary>>({
total: 0,
list: []
});
const isRefreshing = ref(false);
const isLoading = ref(false);
const isNoMore = ref(false);
const isDeleting = ref(false);
const deletingId = ref("");
let loadToken = 0;
const loadStateText = computed(() => {
if (isLoading.value) {
return "正在加载门店日记";
}
if (isNoMore.value) {
return pageResult.list.length ? `共 ${pageResult.total} 篇日记` : "暂无门店日记";
}
return pageResult.list.length ? "上拉加载更多" : "暂无门店日记";
});
function getCurrentMonthListOptions(): StoreDiaryListOptions {
const beginDate = new Date(calendarMonth.year, calendarMonth.month - 1, 1, 0, 0, 0, 0);
const endDate = new Date(calendarMonth.year, calendarMonth.month, 1, 0, 0, 0, 0);
return {
beginCreatedAt: Time.toTimestamp(beginDate),
endCreatedAt: Time.toTimestamp(endDate),
storeId: currentStoreStore.currentStoreId
};
}
async function loadCalendarDiaries(): Promise<void> {
const requestToken = calendarLoadToken + 1;
calendarLoadToken = requestToken;
isCalendarLoading.value = true;
try {
const result = await StoreDiaryAPI.list({
index: 0,
size: 1000
}, getCurrentMonthListOptions());
if (requestToken !== calendarLoadToken) {
return;
}
calendarDiaryList.value = result.list || [];
calendarRenderKey.value += 1;
} finally {
if (requestToken === calendarLoadToken) {
isCalendarLoading.value = false;
}
}
}
async function loadMore(): Promise<void> {
if (isLoading.value || isNoMore.value) {
return;
}
const requestToken = loadToken + 1;
loadToken = requestToken;
isLoading.value = true;
try {
const result = await StoreDiaryAPI.list(page, getCurrentMonthListOptions());
if (requestToken !== loadToken) {
return;
}
const list = result.list || [];
pageResult.total = result.total || 0;
pageResult.list = pageResult.list.concat(list);
page.index += 1;
isNoMore.value = pageResult.list.length >= pageResult.total || list.length < page.size;
} catch (error) {
Message.error(getErrorMessage(error, "门店日记加载失败"));
} finally {
if (requestToken === loadToken) {
isLoading.value = false;
}
}
}
async function reload(): Promise<void> {
loadToken += 1;
page.index = 0;
pageResult.total = 0;
pageResult.list = [];
isNoMore.value = false;
await loadMore();
}
async function refresh(): Promise<void> {
isRefreshing.value = true;
try {
await Promise.all([reload(), loadCalendarDiaries()]);
} finally {
isRefreshing.value = false;
}
}
// ---------- 展示 ----------
function getDiaryTimestamp(item: StoreDiary): number {
return item.createdAt || item.updatedAt || 0;
}
function getDiaryDateText(item: StoreDiary): string {
const date = Time.toDateObject(getDiaryTimestamp(item));
return date ? Time.formatDate(date) : "";
}
function getTitle(item: StoreDiary): string {
return item.title?.trim() || "未命名日记";
}
function getDescription(item: StoreDiary): string {
return item.description?.trim() || "暂无摘要";
}
// ---------- 导航 ----------
async function handleCalendarPanelChange(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);
diaryPopupVisible.value = false;
await Promise.all([reload(), loadCalendarDiaries()]);
}
async function selectCalendarDate(date: Date): Promise<void> {
selectedCalendarDate.value = date;
const diaries = diaryDateMap.value.get(Time.formatDate(date)) || [];
if (diaries.length < 1) {
return;
}
if (diaries.length < 2) {
const [diary] = diaries;
if (diary) {
await toDetail(diary);
}
return;
}
selectedDateText.value = Time.formatDate(date);
selectedDateDiaries.value = [...diaries];
diaryPopupVisible.value = true;
}
function closeDiaryPopup(): void {
diaryPopupVisible.value = false;
}
async function toSelectedDiary(item: StoreDiary): Promise<void> {
closeDiaryPopup();
await toDetail(item);
}
async function toCreate(): Promise<void> {
await router.push({ name: "StoreDiaryCreate" });
}
async function toDetail(item: StoreDiary): Promise<void> {
if (!item.id) {
return;
}
await router.push({
name: "StoreDiaryDetail",
params: { id: item.id }
});
}
// ---------- 删除 ----------
async function confirmDelete(item: StoreDiary): Promise<void> {
if (!item.id || !canDelete.value || isDeleting.value) {
return;
}
const diaryId = item.id;
const dialog = DialogPlugin.confirm({
title: "删除门店日记",
content: `确定删除“${getTitle(item)}”吗?删除后无法恢复。`,
cancelBtn: "取消",
confirmBtn: "删除",
onCancel() {
dialog.destroy();
},
async onConfirm() {
dialog.destroy();
isDeleting.value = true;
deletingId.value = diaryId;
try {
await StoreDiaryAPI.remove(diaryId);
await Promise.all([reload(), loadCalendarDiaries()]);
Message.success("门店日记已删除");
} catch (error) {
Message.error(getErrorMessage(error, "门店日记删除失败"));
} finally {
isDeleting.value = false;
deletingId.value = "";
}
}
});
}
// ---------- 生命周期 ----------
async function preparePage(): Promise<void> {
await currentStoreStore.loadLoginStore();
await Promise.all([reload(), loadCalendarDiaries()]);
}
onMounted(async () => {
navBarStore.clearRight();
navBarStore.setCustomRightVisible(canShowActionMenu.value);
try {
await preparePage();
} catch (error) {
Message.error(getErrorMessage(error, "门店日记页面加载失败"));
}
});
onUnmounted(() => navBarStore.clearRight());
</script>
<style scoped lang="less">
.store-diary-list {
padding: calc(var(--app-nav-height) + 1rem) 0 1rem;
.list-wrapper {
min-height: 70vh;
.calendar {
margin: 0 1rem 1rem;
overflow: hidden;
background: var(--td-bg-color-container);
box-shadow: var(--td-shadow-1);
border-radius: .75rem;
:deep(.t-calendar__title) {
display: none;
}
:deep(.t-calendar-header) {
padding-top: .5rem;
}
:deep(.t-calendar) {
overflow: hidden;
border-radius: .75rem;
&__months {
height: auto;
}
&__dates-item {
height: 2.5rem;
margin-top: 0;
align-items: unset;
}
}
:deep(.has-diary) {
font-weight: 700;
--td-calendar-item-suffix-color: var(--td-brand-color);
}
}
.items {
overflow: hidden;
}
}
.load-state {
padding: 1rem;
color: var(--td-text-color-placeholder);
font-size: .8125rem;
text-align: center;
}
}
.cell {
:deep(.t-cell__title) {
min-width: 0;
overflow: hidden;
}
:deep(.t-cell__title-text),
:deep(.t-cell__description) {
overflow: hidden;
min-width: 0;
white-space: nowrap;
text-overflow: ellipsis;
}
:deep(.t-cell__note) {
flex: 0 0 auto;
min-width: 0;
white-space: nowrap;
}
}
.diary-popup {
padding-bottom: .5rem;
.popup-empty {
padding: 2rem 1rem;
color: var(--td-text-color-placeholder);
text-align: center;
}
}
:global(html.theme-dark) {
.store-diary-list {
.description {
color: #94a3b8;
}
}
}
</style>
+30
View File
@@ -34,6 +34,19 @@
<t-icon name="shop-3" />
</template>
</t-cell>
<t-cell
v-if="canManageStoreDiary"
class="action"
title="门店日记"
note="记录门店经营和护理过程"
arrow
hover
@click="goStoreDiaryManagement"
>
<template #leftIcon>
<t-icon name="file" />
</template>
</t-cell>
</t-cell-group>
</section>
<store-data-charts :key="`${currentStoreStore.currentStoreId}-${chartRefreshKey}`" />
@@ -79,6 +92,9 @@ const canManageStore = computed(() => userStore.hasRoles([
gaoRoleCodes.globalManager,
gaoRoleCodes.storeManager
], "or") && userStore.hasPermissions(permissionCodes.store.read));
const canManageStoreDiary = computed(() => userStore.hasRoles(gaoRoleCodes.storeManager)
&& !userStore.hasRoles(gaoRoleCodes.globalManager)
&& userStore.hasPermissions(permissionCodes.storeDiary.read));
// ---------- 门店切换 ----------
@@ -138,6 +154,20 @@ async function goStoreManagement(): Promise<void> {
await router.push(`/management/stores/${store.id}`);
}
async function goStoreDiaryManagement(): Promise<void> {
if (!canManageStoreDiary.value) {
return;
}
const store = await currentStoreStore.loadLoginStore();
if (!store?.id) {
Message.warning("未绑定所属门店");
return;
}
await router.push({ name: "StoreDiaryList" });
}
onMounted(async () => {
navBarStore.setRightIcon("refresh", refreshCharts);
if (canSelectStore.value) {