fix store diary style

This commit is contained in:
Timi
2026-09-01 14:07:23 +08:00
parent 2d94abfb32
commit 9043f7600d
6 changed files with 314 additions and 65 deletions
+4
View File
@@ -22,6 +22,7 @@
/> />
</t-button> </t-button>
<div <div
v-if="showTypeLabel"
class="type-label white glass-black clip-text" class="type-label white glass-black clip-text"
v-text="getAttachTypeText(item.attachType)" v-text="getAttachTypeText(item.attachType)"
/> />
@@ -165,6 +166,7 @@ interface Props {
showViewerActions?: boolean; showViewerActions?: boolean;
showAdd?: boolean; showAdd?: boolean;
showRemove?: boolean; showRemove?: boolean;
showTypeLabel?: boolean;
submit?: (photoItems: PhotoItem[]) => Promise<void>; submit?: (photoItems: PhotoItem[]) => Promise<void>;
title?: string; title?: string;
} }
@@ -181,6 +183,7 @@ type ViewerPhotoItem = PhotoItem & {
}; };
const ATTACH_TYPE_TEXT_MAP: Record<string, string> = { const ATTACH_TYPE_TEXT_MAP: Record<string, string> = {
ADDITIONAL: "附加照片",
PHOTO: "照片", PHOTO: "照片",
POINT_CARD: "积分卡", POINT_CARD: "积分卡",
OTHER: "其他文件" OTHER: "其他文件"
@@ -196,6 +199,7 @@ const props = withDefaults(defineProps<Props>(), {
required: false, required: false,
showAdd: true, showAdd: true,
showRemove: true, showRemove: true,
showTypeLabel: true,
showViewerActions: true showViewerActions: true
}); });
+5
View File
@@ -423,7 +423,12 @@ onUnmounted(() => navBarStore.clearRight());
} }
:deep(.content) { :deep(.content) {
padding: 0;
overflow: visible; overflow: visible;
.value {
border-radius: 0;
}
} }
:deep(.toolbar) { :deep(.toolbar) {
+31 -1
View File
@@ -11,6 +11,18 @@
underline underline
/> />
</header> </header>
<div v-if="additionalPhotoItemList.length" class="additional-photos-wrapper">
<photo-scroller
:model-value="additionalPhotoList"
:photo-items="additionalPhotoItemList"
:show-add="false"
:show-remove="false"
:show-type-label="false"
:show-viewer-actions="false"
title="附加照片"
v-entrance-transition="140"
/>
</div>
<section class="content" v-entrance-transition="70"> <section class="content" v-entrance-transition="70">
<article-view <article-view
:article="article" :article="article"
@@ -57,11 +69,13 @@
import { ArticleView } from "timi-tdesign-mobile"; import { ArticleView } from "timi-tdesign-mobile";
import { DialogPlugin } from "tdesign-mobile-vue"; import { DialogPlugin } from "tdesign-mobile-vue";
import { PassTimeLabel } from "timi-web"; import { PassTimeLabel } from "timi-web";
import { UserAPI, useUserStore } from "timi-web-api"; import { UserAPI, type Attachment, useUserStore } from "timi-web-api";
import StoreDiaryAPI from "@/api/StoreDiaryAPI"; import StoreDiaryAPI from "@/api/StoreDiaryAPI";
import PhotoScroller from "@/components/PhotoScroller.vue";
import { useCurrentStoreStore } from "@/store/currentStoreStore"; import { useCurrentStoreStore } from "@/store/currentStoreStore";
import { useNavBarStore } from "@/store/navBarStore"; import { useNavBarStore } from "@/store/navBarStore";
import type { StoreDiary } from "@/types/StoreDiary"; import type { StoreDiary } from "@/types/StoreDiary";
import type { PhotoItem } from "@/types/Photo";
import { getAttachmentReadURL } from "@/utils/attachment"; import { getAttachmentReadURL } from "@/utils/attachment";
import { buildFallbackAvatar } from "@/utils/avatar"; import { buildFallbackAvatar } from "@/utils/avatar";
import { getErrorMessage } from "@/utils/error"; import { getErrorMessage } from "@/utils/error";
@@ -87,6 +101,16 @@ const isLoading = ref(false);
const isDeleting = ref(false); const isDeleting = ref(false);
let loadToken = 0; let loadToken = 0;
const additionalPhotoItemList = computed<PhotoItem[]>(() => (article.value?.attachmentList || [])
.filter(item => item.attachType === "ADDITIONAL" && !!item.id)
.map(item => ({
attachment: item as Partial<Attachment>,
attachType: "ADDITIONAL",
previewUrl: getAttachmentReadURL(item.id || ""),
url: getAttachmentReadURL(item.id || "", 90)
})));
const additionalPhotoList = computed(() => additionalPhotoItemList.value.map(item => item.url));
function resolveAttachmentUrl(attachmentId: string): string { function resolveAttachmentUrl(attachmentId: string): string {
return getAttachmentReadURL(attachmentId); return getAttachmentReadURL(attachmentId);
} }
@@ -222,6 +246,12 @@ onUnmounted(() => navBarStore.clearRight());
} }
} }
.additional-photos-wrapper {
margin: 1rem 1rem 0 1rem;
border-bottom: 1px solid rgba(148, 163, 184, .2);
padding-bottom: 1rem;
}
.content { .content {
padding: 1rem; padding: 1rem;
+126 -8
View File
@@ -21,6 +21,26 @@
placeholder="可填写本篇日记摘要" placeholder="可填写本篇日记摘要"
/> />
</t-form-item> </t-form-item>
<section class="photo-wrapper" v-entrance-transition="140">
<photo-scroller
v-model="photoList"
v-model:photo-items="photoItemList"
title="附加照片"
:edit-mode="true"
:loading="isPhotoUploading"
:show-type-label="false"
:show-viewer-actions="false"
@add="openPhotoPicker"
/>
<input
ref="photoInputRef"
class="file-input"
type="file"
accept="image/*"
multiple
@change="handlePhotoSelected"
>
</section>
<section class="editor-wrapper" v-entrance-transition="70"> <section class="editor-wrapper" v-entrance-transition="70">
<article-editor <article-editor
ref="articleEditorRef" ref="articleEditorRef"
@@ -44,13 +64,15 @@ import {
type ArticleEditorExpose, type ArticleEditorExpose,
type UploadResult type UploadResult
} from "timi-tdesign-mobile"; } from "timi-tdesign-mobile";
import { AttachmentAPI, useUserStore } from "timi-web-api"; import { AttachmentAPI, type Attachment, useUserStore } from "timi-web-api";
import StoreDiaryAPI from "@/api/StoreDiaryAPI"; import StoreDiaryAPI from "@/api/StoreDiaryAPI";
import PhotoScroller from "@/components/PhotoScroller.vue";
import { useCurrentStoreStore } from "@/store/currentStoreStore"; import { useCurrentStoreStore } from "@/store/currentStoreStore";
import { useNavBarStore } from "@/store/navBarStore"; import { useNavBarStore } from "@/store/navBarStore";
import type { StoreDiary } from "@/types/StoreDiary"; import type { StoreDiary } from "@/types/StoreDiary";
import type { PhotoItem } from "@/types/Photo";
import { getErrorMessage } from "@/utils/error"; import { getErrorMessage } from "@/utils/error";
import { compressUploadImage } from "@/utils/imageCompression"; import { compressUploadImage, compressUploadImageList } from "@/utils/imageCompression";
import { getAttachmentReadURL } from "@/utils/attachment"; import { getAttachmentReadURL } from "@/utils/attachment";
import { Message } from "@/utils/message"; import { Message } from "@/utils/message";
import { permissionCodes } from "@/utils/permission"; import { permissionCodes } from "@/utils/permission";
@@ -105,6 +127,83 @@ function updateArticle(value: ArticleEditorArticle): void {
}; };
} }
// ---------- 附加照片 ----------
const photoInputRef = ref<HTMLInputElement>();
const photoItemList = ref<PhotoItem[]>([]);
const isPhotoUploading = ref(false);
const photoList = computed<string[]>({
get: () => photoItemList.value.map(item => item.url),
set: value => {
const urlSet = new Set(value);
photoItemList.value = photoItemList.value.filter(item => urlSet.has(item.url));
}
});
function openPhotoPicker(): void {
photoInputRef.value?.click();
}
async function handlePhotoSelected(event: Event): Promise<void> {
const input = event.target as HTMLInputElement;
const fileList = Array.from(input.files || []);
if (!fileList.length) {
return;
}
isPhotoUploading.value = true;
try {
const tempFileList = await AttachmentAPI.upload(await compressUploadImageList(fileList));
for (const tempFile of tempFileList) {
if (!tempFile.id) {
continue;
}
photoItemList.value.push({
tempFileId: tempFile.id,
attachType: "ADDITIONAL",
url: AttachmentAPI.getTempReadURL(tempFile.id)
});
}
} catch (error) {
Message.error(getErrorMessage(error, "附加照片上传失败"));
} finally {
isPhotoUploading.value = false;
input.value = "";
}
}
function buildPhotoItemList(attachmentList?: StoreDiary["attachmentList"]): PhotoItem[] {
return (attachmentList || []).flatMap(item => {
if (item.attachType !== "ADDITIONAL" || !item.id) {
return [];
}
return [{
attachment: item as Partial<Attachment>,
attachType: "ADDITIONAL",
previewUrl: getAttachmentReadURL(item.id),
url: getAttachmentReadURL(item.id, 90)
}];
});
}
function buildAdditionalAttachmentList(): Attachment[] {
return photoItemList.value.flatMap(item => {
if (item.attachment) {
return [{
...item.attachment,
attachType: "ADDITIONAL"
}];
}
if (item.tempFileId) {
return [{
attachType: "ADDITIONAL",
tempFileId: item.tempFileId
}];
}
return [];
});
}
const selectedStoreName = computed(() => { const selectedStoreName = computed(() => {
const store = currentStoreStore.storeList.find(item => item.id === article.value.bizId) const store = currentStoreStore.storeList.find(item => item.id === article.value.bizId)
|| (currentStoreStore.loginStore?.id === article.value.bizId ? currentStoreStore.loginStore : undefined); || (currentStoreStore.loginStore?.id === article.value.bizId ? currentStoreStore.loginStore : undefined);
@@ -187,7 +286,10 @@ async function save(): Promise<void> {
description: article.value.description?.trim() || "", description: article.value.description?.trim() || "",
contentType: normalizeContentType(editorValue?.contentType ?? article.value.contentType), contentType: normalizeContentType(editorValue?.contentType ?? article.value.contentType),
content: editorValue?.content ?? article.value.content ?? "", content: editorValue?.content ?? article.value.content ?? "",
attachmentList: editorValue?.attachmentList ?? article.value.attachmentList ?? [], attachmentList: [
...(editorValue?.attachmentList ?? article.value.attachmentList ?? []).filter(item => item.attachType !== "ADDITIONAL"),
...buildAdditionalAttachmentList()
],
status: article.value.status || "NORMAL" status: article.value.status || "NORMAL"
}; };
if (!validateArticle(value)) { if (!validateArticle(value)) {
@@ -234,6 +336,7 @@ async function loadArticle(): Promise<void> {
...createEmptyArticle(), ...createEmptyArticle(),
bizId: article.value.bizId || currentStoreStore.currentStoreId bizId: article.value.bizId || currentStoreStore.currentStoreId
}; };
photoItemList.value = [];
return; return;
} }
@@ -246,6 +349,7 @@ async function loadArticle(): Promise<void> {
content: result.content || "", content: result.content || "",
attachmentList: result.attachmentList || [] attachmentList: result.attachmentList || []
}; };
photoItemList.value = buildPhotoItemList(result.attachmentList);
} }
onMounted(async () => { onMounted(async () => {
@@ -278,9 +382,10 @@ onUnmounted(() => navBarStore.clearRight());
.editor-wrapper { .editor-wrapper {
width: 100%; width: 100%;
overflow: visible;
min-width: 0; min-width: 0;
max-width: 100%; max-width: 100%;
overflow: visible; border-top: 1px solid var(--td-component-border);
.editor { .editor {
width: 100%; width: 100%;
@@ -292,23 +397,28 @@ onUnmounted(() => navBarStore.clearRight());
:deep(.tp-editor) { :deep(.tp-editor) {
width: 100%; width: 100%;
height: auto;
min-width: 0; min-width: 0;
max-width: 100%; max-width: 100%;
height: auto;
overflow: visible; overflow: visible;
} }
:deep(.content) { :deep(.content) {
padding: 0;
overflow: visible; overflow: visible;
.value {
border-radius: 0;
}
} }
:deep(.toolbar) { :deep(.toolbar) {
width: 100%;
min-width: 0;
max-width: 100%;
top: var(--app-nav-height); top: var(--app-nav-height);
width: 100%;
z-index: 10; z-index: 10;
position: sticky; position: sticky;
min-width: 0;
max-width: 100%;
} }
:deep(.toolbar .scroll) { :deep(.toolbar .scroll) {
@@ -317,6 +427,14 @@ onUnmounted(() => navBarStore.clearRight());
max-width: 100%; max-width: 100%;
} }
} }
.photo-wrapper {
padding: 1rem;
.file-input {
display: none;
}
}
} }
} }
</style> </style>
+41 -56
View File
@@ -12,6 +12,7 @@
:key="calendarRenderKey" :key="calendarRenderKey"
class="calendar-view" class="calendar-view"
:value="selectedCalendarDate" :value="selectedCalendarDate"
:min-date="calendarMinDate"
:format="calendarFormat" :format="calendarFormat"
switch-mode="month" switch-mode="month"
:use-popup="false" :use-popup="false"
@@ -26,17 +27,7 @@
class="item" class="item"
v-entrance-transition="index * 30" v-entrance-transition="index * 30"
> >
<t-cell <store-diary-list-item :diary="item" @select="toDetail" />
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> <template v-if="canDelete" #right>
<t-button <t-button
class="swipe-cell-button" class="swipe-cell-button"
@@ -62,21 +53,14 @@
@left-button-click="closeDiaryPopup" @left-button-click="closeDiaryPopup"
> >
<div class="diary-popup"> <div class="diary-popup">
<t-cell-group v-if="selectedDateDiaries.length" theme="card"> <t-cell-group v-if="selectedDateDiaries.length">
<t-cell <store-diary-list-item
v-for="(item, index) in selectedDateDiaries" v-for="(item, index) in selectedDateDiaries"
:key="item.id || index" :key="item.id || index"
class="cell" :diary="item"
:title="getTitle(item)"
:description="getDescription(item)"
arrow
hover hover
@click="toSelectedDiary(item)" @select="toSelectedDiary"
> />
<template #note>
<pass-time-label :timestamp="item.updatedAt || item.createdAt" underline />
</template>
</t-cell>
</t-cell-group> </t-cell-group>
<div v-else class="popup-empty" v-text="'该日期暂无门店日记'" /> <div v-else class="popup-empty" v-text="'该日期暂无门店日记'" />
</div> </div>
@@ -87,10 +71,11 @@
import { BottomPopup } from "timi-tdesign-mobile"; import { BottomPopup } from "timi-tdesign-mobile";
import { type CalendarProps, DialogPlugin, type TDate } from "tdesign-mobile-vue"; import { type CalendarProps, DialogPlugin, type TDate } from "tdesign-mobile-vue";
import { type PageResult, useUserStore } from "timi-web-api"; import { type PageResult, useUserStore } from "timi-web-api";
import { PassTimeLabel, Time } from "timi-web"; import { Time } from "timi-web";
import StoreDiaryAPI, { type StoreDiaryListOptions } from "@/api/StoreDiaryAPI"; import StoreDiaryAPI, { type StoreDiaryListOptions } from "@/api/StoreDiaryAPI";
import { useCurrentStoreStore } from "@/store/currentStoreStore"; import { useCurrentStoreStore } from "@/store/currentStoreStore";
import { useNavBarStore } from "@/store/navBarStore"; import { useNavBarStore } from "@/store/navBarStore";
import StoreDiaryListItem from "@/pages/store/component/StoreDiaryListItem.vue";
import type { NavBarAction } from "@/types/NavBarAction"; import type { NavBarAction } from "@/types/NavBarAction";
import type { StoreDiary } from "@/types/StoreDiary"; import type { StoreDiary } from "@/types/StoreDiary";
import { getErrorMessage } from "@/utils/error"; import { getErrorMessage } from "@/utils/error";
@@ -102,6 +87,7 @@ defineOptions({
}); });
const router = useRouter(); const router = useRouter();
const route = useRoute();
const navBarStore = useNavBarStore(); const navBarStore = useNavBarStore();
const userStore = useUserStore(); const userStore = useUserStore();
const currentStoreStore = useCurrentStoreStore(); const currentStoreStore = useCurrentStoreStore();
@@ -112,10 +98,12 @@ const canShowActionMenu = computed(() => canCreate.value);
// ---------- 日历 ---------- // ---------- 日历 ----------
const today = new Date(); const today = new Date();
const selectedCalendarDate = ref<Date>(today); const calendarMinDate = new Date(today.getFullYear() - 2, 0, 1);
const initialCalendarDate = parseCalendarDate(route.query.calendarDate);
const selectedCalendarDate = ref<Date>(initialCalendarDate);
const calendarMonth = reactive({ const calendarMonth = reactive({
year: today.getFullYear(), year: initialCalendarDate.getFullYear(),
month: today.getMonth() + 1 month: initialCalendarDate.getMonth() + 1
}); });
const calendarRenderKey = ref(0); const calendarRenderKey = ref(0);
const calendarDiaryList = ref<StoreDiary[]>([]); const calendarDiaryList = ref<StoreDiary[]>([]);
@@ -290,12 +278,24 @@ function getDiaryDateText(item: StoreDiary): string {
return date ? Time.formatDate(date) : ""; return date ? Time.formatDate(date) : "";
} }
function getTitle(item: StoreDiary): string { function parseCalendarDate(value: unknown): Date {
return item.title?.trim() || "未命名日记"; const text = Array.isArray(value) ? String(value[0] || "") : String(value || "");
} const [year, month, day] = text.split("-").map(Number);
const date = new Date(year, month - 1, day);
function getDescription(item: StoreDiary): string { if (
return item.description?.trim() || "暂无摘要"; !Number.isInteger(year)
|| !Number.isInteger(month)
|| !Number.isInteger(day)
|| month < 1
|| 12 < month
|| day < 1
|| date.getFullYear() !== year
|| date.getMonth() + 1 !== month
|| date.getDate() !== day
) {
return today;
}
return date;
} }
// ---------- 导航 ---------- // ---------- 导航 ----------
@@ -347,6 +347,13 @@ async function toDetail(item: StoreDiary): Promise<void> {
if (!item.id) { if (!item.id) {
return; return;
} }
await router.replace({
name: "StoreDiaryList",
query: {
...route.query,
calendarDate: getDiaryDateText(item) || Time.formatDate(selectedCalendarDate.value)
}
});
await router.push({ await router.push({
name: "StoreDiaryDetail", name: "StoreDiaryDetail",
params: { id: item.id } params: { id: item.id }
@@ -363,7 +370,7 @@ async function confirmDelete(item: StoreDiary): Promise<void> {
const diaryId = item.id; const diaryId = item.id;
const dialog = DialogPlugin.confirm({ const dialog = DialogPlugin.confirm({
title: "删除门店日记", title: "删除门店日记",
content: `确定删除“${getTitle(item)}”吗?删除后无法恢复。`, content: `确定删除“${item.title?.trim() || "未命名日记"}”吗?删除后无法恢复。`,
cancelBtn: "取消", cancelBtn: "取消",
confirmBtn: "删除", confirmBtn: "删除",
onCancel() { onCancel() {
@@ -463,28 +470,6 @@ onUnmounted(() => navBarStore.clearRight());
} }
} }
.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 { .diary-popup {
padding-bottom: .5rem; padding-bottom: .5rem;
@@ -0,0 +1,107 @@
<template>
<t-cell
class="cell"
:title="getTitle(props.diary)"
:description="getDescription(props.diary)"
:arrow="true"
:hover="props.hover"
@click="handleSelect"
>
<template #leftIcon>
<t-image
v-if="getCoverUrl(props.diary)"
:src="getCoverUrl(props.diary)"
alt="文章首图"
fit="cover"
class="cover"
/>
<div v-else class="cover placeholder">
<t-icon name="article" size="2rem" />
</div>
</template>
<template #note>
<pass-time-label :timestamp="props.diary.updatedAt || props.diary.createdAt" underline />
</template>
</t-cell>
</template>
<script setup lang="ts">
import { PassTimeLabel } from "timi-web";
import type { StoreDiary } from "@/types/StoreDiary";
import { getAttachmentReadURL } from "@/utils/attachment";
defineOptions({
name: "StoreDiaryListItem"
});
interface Props {
diary: StoreDiary;
hover?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
hover: false
});
const emit = defineEmits<{
select: [diary: StoreDiary];
}>();
function getTitle(item: StoreDiary): string {
return item.title?.trim() || "未命名日记";
}
function getDescription(item: StoreDiary): string {
return item.description?.trim() || "暂无摘要";
}
function getCoverUrl(item: StoreDiary): string {
const attachment = item.attachmentList?.[0];
return attachment?.id ? getAttachmentReadURL(attachment.id, 128) : "";
}
function handleSelect(): void {
emit("select", props.diary);
}
</script>
<style scoped lang="less">
.cell {
padding: 10px;
: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;
}
.cover {
flex: none;
width: 3.5rem;
height: 3.5rem;
overflow: hidden;
border-radius: .75rem;
}
.placeholder {
color: var(--td-text-color-placeholder);
display: flex;
background: var(--td-bg-color-secondarycontainer);
align-items: center;
justify-content: center;
}
}
</style>