add StoreDiary
This commit is contained in:
@@ -0,0 +1,54 @@
|
|||||||
|
import { axios, type Page, type PageResult } from "timi-web-api";
|
||||||
|
import type { StoreDiary } from "@/types/StoreDiary";
|
||||||
|
|
||||||
|
const BASE_URI = "/gao/store/diary";
|
||||||
|
|
||||||
|
export interface StoreDiaryListOptions {
|
||||||
|
storeId?: string;
|
||||||
|
beginCreatedAt: number;
|
||||||
|
endCreatedAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 门店日记接口。
|
||||||
|
*
|
||||||
|
* @param page 分页参数
|
||||||
|
* @param options 门店和月份筛选条件
|
||||||
|
*/
|
||||||
|
async function list(page: Page<StoreDiary>, options: StoreDiaryListOptions): Promise<PageResult<StoreDiary>> {
|
||||||
|
return await axios.post(`${BASE_URI}/list`, {
|
||||||
|
index: page.index,
|
||||||
|
size: page.size,
|
||||||
|
beginCreatedAt: options.beginCreatedAt,
|
||||||
|
endCreatedAt: options.endCreatedAt,
|
||||||
|
...(options.storeId ? { storeId: options.storeId } : {})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function detail(id: string): Promise<StoreDiary> {
|
||||||
|
return await axios.post(`${BASE_URI}/detail`, undefined, {
|
||||||
|
params: { id }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function create(article: StoreDiary): Promise<string> {
|
||||||
|
return await axios.post(`${BASE_URI}/create`, article);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function update(article: StoreDiary): Promise<void> {
|
||||||
|
await axios.post(`${BASE_URI}/update`, article);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function remove(id: string): Promise<void> {
|
||||||
|
await axios.post(`${BASE_URI}/delete`, undefined, {
|
||||||
|
params: { id }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
list,
|
||||||
|
detail,
|
||||||
|
create,
|
||||||
|
update,
|
||||||
|
remove
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -34,6 +34,19 @@
|
|||||||
<t-icon name="shop-3" />
|
<t-icon name="shop-3" />
|
||||||
</template>
|
</template>
|
||||||
</t-cell>
|
</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>
|
</t-cell-group>
|
||||||
</section>
|
</section>
|
||||||
<store-data-charts :key="`${currentStoreStore.currentStoreId}-${chartRefreshKey}`" />
|
<store-data-charts :key="`${currentStoreStore.currentStoreId}-${chartRefreshKey}`" />
|
||||||
@@ -79,6 +92,9 @@ const canManageStore = computed(() => userStore.hasRoles([
|
|||||||
gaoRoleCodes.globalManager,
|
gaoRoleCodes.globalManager,
|
||||||
gaoRoleCodes.storeManager
|
gaoRoleCodes.storeManager
|
||||||
], "or") && userStore.hasPermissions(permissionCodes.store.read));
|
], "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}`);
|
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 () => {
|
onMounted(async () => {
|
||||||
navBarStore.setRightIcon("refresh", refreshCharts);
|
navBarStore.setRightIcon("refresh", refreshCharts);
|
||||||
if (canSelectStore.value) {
|
if (canSelectStore.value) {
|
||||||
|
|||||||
+12
-1
@@ -124,12 +124,23 @@ router.beforeEach(async (to) => {
|
|||||||
|
|
||||||
async function resolveStoreAccessRedirect(to: RouteLocationNormalized): Promise<string | undefined> {
|
async function resolveStoreAccessRedirect(to: RouteLocationNormalized): Promise<string | undefined> {
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
const routeName = String(to.name || "");
|
||||||
|
const storeDiaryRouteNameList = ["StoreDiaryList", "StoreDiaryCreate", "StoreDiaryDetail", "StoreDiaryEdit"];
|
||||||
|
if (storeDiaryRouteNameList.includes(routeName)) {
|
||||||
|
if (!userStore.hasRoles(gaoRoleCodes.storeManager) || userStore.hasRoles(gaoRoleCodes.globalManager)) {
|
||||||
|
return "/management";
|
||||||
|
}
|
||||||
|
const currentStoreStore = useCurrentStoreStore();
|
||||||
|
const store = await currentStoreStore.loadLoginStore();
|
||||||
|
return store?.id ? undefined : "/management";
|
||||||
|
}
|
||||||
|
|
||||||
if (!userStore.hasRoles(gaoRoleCodes.storeManager) || userStore.hasRoles(gaoRoleCodes.globalManager)) {
|
if (!userStore.hasRoles(gaoRoleCodes.storeManager) || userStore.hasRoles(gaoRoleCodes.globalManager)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const storeRouteNameList = ["StoreList", "StoreCreate", "StoreSort", "StoreEdit", "StoreBusinessDayCalendar"];
|
const storeRouteNameList = ["StoreList", "StoreCreate", "StoreSort", "StoreEdit", "StoreBusinessDayCalendar"];
|
||||||
if (!storeRouteNameList.includes(String(to.name || ""))) {
|
if (!storeRouteNameList.includes(routeName)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ import StoreBusinessDayCalendar from "@/pages/store/StoreBusinessDayCalendar.vue
|
|||||||
import StoreEdit from "@/pages/store/StoreEdit.vue";
|
import StoreEdit from "@/pages/store/StoreEdit.vue";
|
||||||
import StoreList from "@/pages/store/StoreList.vue";
|
import StoreList from "@/pages/store/StoreList.vue";
|
||||||
import StoreSort from "@/pages/store/StoreSort.vue";
|
import StoreSort from "@/pages/store/StoreSort.vue";
|
||||||
|
import StoreDiaryDetail from "@/pages/store/StoreDiaryDetail.vue";
|
||||||
|
import StoreDiaryEdit from "@/pages/store/StoreDiaryEdit.vue";
|
||||||
|
import StoreDiaryList from "@/pages/store/StoreDiaryList.vue";
|
||||||
import TagList from "@/pages/tag/TagList.vue";
|
import TagList from "@/pages/tag/TagList.vue";
|
||||||
|
|
||||||
export const storeRoutes: RouteRecordRaw[] = [
|
export const storeRoutes: RouteRecordRaw[] = [
|
||||||
@@ -83,6 +86,62 @@ export const storeRoutes: RouteRecordRaw[] = [
|
|||||||
bodyBackground: "#F5F5F8",
|
bodyBackground: "#F5F5F8",
|
||||||
darkBodyBackground: "#10161f"
|
darkBodyBackground: "#10161f"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/management/store-diaries",
|
||||||
|
name: "StoreDiaryList",
|
||||||
|
component: StoreDiaryList,
|
||||||
|
meta: {
|
||||||
|
depth: 3,
|
||||||
|
navBarVisible: true,
|
||||||
|
navBarCanBack: true,
|
||||||
|
navBarTitle: "门店日记",
|
||||||
|
tabBarVisible: false,
|
||||||
|
bodyBackground: "#F5F5F8",
|
||||||
|
darkBodyBackground: "#10161f"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/management/store-diaries/create",
|
||||||
|
name: "StoreDiaryCreate",
|
||||||
|
component: StoreDiaryEdit,
|
||||||
|
meta: {
|
||||||
|
depth: 4,
|
||||||
|
navBarVisible: true,
|
||||||
|
navBarCanBack: true,
|
||||||
|
navBarTitle: "新增门店日记",
|
||||||
|
tabBarVisible: false,
|
||||||
|
bodyBackground: "#F5F5F8",
|
||||||
|
darkBodyBackground: "#10161f"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/management/store-diaries/:id",
|
||||||
|
name: "StoreDiaryDetail",
|
||||||
|
component: StoreDiaryDetail,
|
||||||
|
meta: {
|
||||||
|
depth: 4,
|
||||||
|
navBarVisible: true,
|
||||||
|
navBarCanBack: true,
|
||||||
|
navBarTitle: "门店日记详情",
|
||||||
|
tabBarVisible: false,
|
||||||
|
bodyBackground: "#FFF",
|
||||||
|
darkBodyBackground: "#000"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/management/store-diaries/:id/edit",
|
||||||
|
name: "StoreDiaryEdit",
|
||||||
|
component: StoreDiaryEdit,
|
||||||
|
meta: {
|
||||||
|
depth: 5,
|
||||||
|
navBarVisible: true,
|
||||||
|
navBarCanBack: true,
|
||||||
|
navBarTitle: "编辑门店日记",
|
||||||
|
tabBarVisible: false,
|
||||||
|
bodyBackground: "#F5F5F8",
|
||||||
|
darkBodyBackground: "#10161f"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import type {
|
||||||
|
Article as ArticleEditorArticle,
|
||||||
|
ArticleContentType,
|
||||||
|
Attachment as ArticleAttachment
|
||||||
|
} from "timi-tdesign-mobile";
|
||||||
|
import type { User } from "timi-web-api";
|
||||||
|
|
||||||
|
/** 门店日记正文类型。 */
|
||||||
|
export type StoreDiaryContentType = ArticleContentType;
|
||||||
|
|
||||||
|
/** 门店日记附件。 */
|
||||||
|
export type StoreDiaryAttachment = ArticleAttachment;
|
||||||
|
|
||||||
|
/** 门店日记实体。 */
|
||||||
|
export type StoreDiary = ArticleEditorArticle & {
|
||||||
|
id?: string;
|
||||||
|
bizType?: "GAO_STORE_DIARY";
|
||||||
|
bizId?: string;
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
contentType?: StoreDiaryContentType;
|
||||||
|
content?: string;
|
||||||
|
attachmentList?: StoreDiaryAttachment[];
|
||||||
|
status?: "DART" | "NORMAL" | "HIDDEN";
|
||||||
|
createdAt?: number;
|
||||||
|
updatedAt?: number;
|
||||||
|
updater?: User;
|
||||||
|
};
|
||||||
@@ -6,6 +6,12 @@ export const permissionCodes = {
|
|||||||
update: "GAO:STORE:UPDATE",
|
update: "GAO:STORE:UPDATE",
|
||||||
delete: "GAO:STORE:DELETE"
|
delete: "GAO:STORE:DELETE"
|
||||||
},
|
},
|
||||||
|
storeDiary: {
|
||||||
|
create: "GAO:STORE_DIARY:CREATE",
|
||||||
|
read: "GAO:STORE_DIARY:READ",
|
||||||
|
update: "GAO:STORE_DIARY:UPDATE",
|
||||||
|
delete: "GAO:STORE_DIARY:DELETE"
|
||||||
|
},
|
||||||
customer: {
|
customer: {
|
||||||
create: "GAO:CUSTOMER:CREATE",
|
create: "GAO:CUSTOMER:CREATE",
|
||||||
read: "GAO:CUSTOMER:READ",
|
read: "GAO:CUSTOMER:READ",
|
||||||
@@ -112,6 +118,10 @@ export const gaoStoreScopePermissionCodes = [
|
|||||||
permissionCodes.customer.export,
|
permissionCodes.customer.export,
|
||||||
permissionCodes.customer.update,
|
permissionCodes.customer.update,
|
||||||
permissionCodes.customer.delete,
|
permissionCodes.customer.delete,
|
||||||
|
permissionCodes.storeDiary.create,
|
||||||
|
permissionCodes.storeDiary.read,
|
||||||
|
permissionCodes.storeDiary.update,
|
||||||
|
permissionCodes.storeDiary.delete,
|
||||||
permissionCodes.event.create,
|
permissionCodes.event.create,
|
||||||
permissionCodes.event.read,
|
permissionCodes.event.read,
|
||||||
permissionCodes.event.update,
|
permissionCodes.event.update,
|
||||||
|
|||||||
Reference in New Issue
Block a user