Files
youlai-app/src/pages/work/notice/index.vue
Ray.Hao e5cd415f66 chore: 升级 wot-design-uni 到 @wot-ui/ui v2.0.8
重大变更:
- 升级 UI 组件库从 wot-design-uni@1.14.0 到 @wot-ui/ui@2.0.8
- 新增 dayjs@1.11.13 作为独立依赖

组件迁移:
- wd-message-box → wd-dialog (3个文件)
- wd-status-tip → wd-empty (10个文件)
- wd-col-picker → wd-cascader (3个文件)

API 变更:
- useMessage → useDialog (12个文件)
- 所有导入路径从 wot-design-uni 更新为 @wot-ui/ui
- dayjs 改为独立导入

配置更新:
- vite.config.ts: 自定义 WotUIResolver 替代 WotResolver
- tsconfig.json: 更新类型声明路径
- components.d.ts: 更新所有组件类型路径

影响范围:
- 修改文件: 33个
- 布局文件: 2个
- 页面文件: 21个
- 组件文件: 1个
- 配置文件: 4个
- 类型文件: 2个

Breaking Changes:
- Dialog 的 closeOnClickModal 默认值从 true 改为 false
- Prompt 返回值从 res 改为 res.value
- Empty 的 image 属性改为 icon
- Cascader 的 columns 建议改为 options(向后兼容)
2026-05-16 10:40:27 +08:00

473 lines
13 KiB
Vue

<template>
<view class="page page--padding">
<view>
<wd-search
v-model="queryParams.title"
placeholder="搜索通知标题"
hide-cancel
@search="handleSearch"
/>
</view>
<!-- 通知列表 -->
<view class="mt-16rpx">
<wd-card
v-for="item in pageData"
:key="item.id"
custom-class="item-card"
@click="openNoticeDetail(item)"
>
<!-- 主信息行 -->
<view class="flex-start">
<view class="notice-card__main">
<view class="flex-start mt-12rpx">
<text class="notice-card__title">{{ item.title }}</text>
</view>
<text class="notice-card__publisher">
{{ item.publisherName || "系统管理员" }}
</text>
</view>
<wd-tag :type="getStatusType(item.publishStatus)" plain>
{{ getStatusText(item.publishStatus) }}
</wd-tag>
</view>
<!-- 辅助信息行 -->
<view class="notice-card__meta">
<view class="notice-card__detail">
<wd-icon name="user" size="16" class="color-text-secondary" />
<text class="notice-card__detail-text">
{{ item.targetType === 1 ? "全体" : "指定用户" }}
</text>
</view>
<view class="notice-card__detail">
<wd-icon name="warning" size="16" class="color-text-secondary" />
<text class="notice-card__detail-text">
{{ getLevelText(item.level) }}
</text>
</view>
</view>
<!-- 元信息行 -->
<view class="notice-card__footer">
<text class="notice-card__time">{{ formatTime(item) }}</text>
<view
class="notice-card__action"
hover-class="notice-card__action--hover"
@click.stop="showNoticeActions(item)"
>
<wd-icon name="more" size="16" class="color-text-secondary" />
</view>
</view>
</wd-card>
<wd-loadmore v-if="total > 0" :state="loadMoreState" @reload="fetchNoticeList" />
<wd-empty v-else-if="total === 0" icon="search" tip="暂无数据" />
</view>
<!-- 详情弹窗 -->
<wd-popup
v-model="detailDialog.visible"
position="bottom"
custom-class="popup-bottom"
@close="closeNoticeDetail"
>
<view class="p-4">
<view class="popup-title">通知详情</view>
<wd-cell-group border>
<wd-cell title="标题" :value="noticeDetail.title" />
<wd-cell title="发布状态">
<template #default>
<wd-tag :type="getStatusType(noticeDetail.publishStatus)" size="small">
{{ getStatusText(noticeDetail.publishStatus) }}
</wd-tag>
</template>
</wd-cell>
<wd-cell title="发布人" :value="noticeDetail.publisherName" />
<wd-cell title="发布时间" :value="String(noticeDetail.publishTime || '-')" />
</wd-cell-group>
<view class="mt-4 p-4 bg-[var(--color-bg-secondary)] rounded-lg">
<rich-text :nodes="noticeDetail.content" class="text-28rpx" />
</view>
<view class="popup-actions">
<wd-button type="info" plain block @click="closeNoticeDetail">关闭</wd-button>
</view>
</view>
</wd-popup>
<!-- 新增/编辑弹窗 -->
<wd-popup
v-model="formDialog.visible"
position="bottom"
custom-class="popup-bottom"
@close="closeNoticeForm"
>
<view class="p-4">
<view class="popup-title">
{{ formData.id ? "编辑通知" : "新增通知" }}
</view>
<wd-form ref="formRef" :model="formData" :rules="formRules">
<wd-cell-group border>
<wd-input v-model="formData.title" label="标题" required placeholder="请输入通知标题" />
<wd-cell title="优先级">
<wd-radio-group v-model="formData.level" shape="button">
<wd-radio value="L"></wd-radio>
<wd-radio value="M"></wd-radio>
<wd-radio value="H"></wd-radio>
</wd-radio-group>
</wd-cell>
<wd-cell title="目标类型">
<wd-radio-group v-model="formData.targetType" shape="button">
<wd-radio :value="1">全体</wd-radio>
<wd-radio :value="2">指定用户</wd-radio>
</wd-radio-group>
</wd-cell>
<wd-textarea
v-model="formData.content"
label="内容"
placeholder="请输入通知内容"
:maxlength="500"
show-word-limit
/>
</wd-cell-group>
</wd-form>
<view class="popup-actions">
<wd-button type="info" plain @click="closeNoticeForm">取消</wd-button>
<wd-button type="primary" :loading="isSubmitting" @click="submitNoticeForm">
保存
</wd-button>
</view>
</view>
</wd-popup>
<wd-action-sheet
v-model="actionSheetVisible"
:actions="actionSheetActions"
cancel-text="取消"
@select="handleActionSelect"
/>
<wd-fab
v-if="hasPermission('sys:notice:create') && !formDialog.visible && !detailDialog.visible"
@click="openNoticeForm()"
/>
</view>
</template>
<script lang="ts" setup>
import { onLoad, onReachBottom } from "@dcloudio/uni-app";
import { LoadMoreState } from "@wot-ui/ui/components/wd-loadmore/types";
import { FormRules } from "@wot-ui/ui/components/wd-form/types";
import { useToast, useDialog } from "@wot-ui/ui";
import NoticeAPI, {
type NoticePageQuery,
NoticeItem,
NoticeDetail,
NoticeForm,
} from "@/api/notice";
import { hasPermission } from "@/utils/permission";
const toast = useToast();
const { messageBox } = useDialog();
const loadMoreState = ref<LoadMoreState>("loading");
const formRef = ref();
const isSubmitting = ref(false);
const queryParams = reactive<NoticePageQuery>({ pageNum: 1, pageSize: 10 });
const total = ref(0);
const pageData = ref<NoticeItem[]>([]);
const noticeDetail = ref<NoticeDetail>({});
const detailDialog = reactive({ visible: false });
const formDialog = reactive({ visible: false });
const initialFormData: NoticeForm = {
id: undefined,
title: undefined,
content: undefined,
type: undefined,
level: "M",
targetType: 1,
targetUserIds: undefined,
};
const formData = reactive<NoticeForm>({ ...initialFormData });
const formRules: FormRules = {
title: [{ required: true, message: "请输入通知标题" }],
};
// 获取状态样式
const getStatusType = (
status?: number
): "default" | "primary" | "danger" | "warning" | "success" => {
const map: Record<number, "default" | "primary" | "danger" | "warning" | "success"> = {
0: "primary",
1: "success",
[-1]: "warning",
};
return status !== undefined ? map[status] || "default" : "default";
};
// 获取状态文本
const getStatusText = (status?: number): string => {
const map: Record<number, string> = { 0: "未发布", 1: "已发布", [-1]: "已撤回" };
return status !== undefined ? map[status] || "未知" : "-";
};
// 获取级别文本
const getLevelText = (level?: string | number): string => {
const map: Record<string, string> = { L: "低", M: "中", H: "高" };
return level ? map[String(level)] || String(level) : "-";
};
// 格式化时间
const formatTime = (item: NoticeItem): string => {
if (item.publishStatus === 1 && item.publishTime) {
return String(item.publishTime);
}
if (item.publishStatus === -1 && item.revokeTime) {
return String(item.revokeTime);
}
return "-";
};
// 搜索触发
const handleSearch = () => loadNoticeList();
// 加载列表
function loadNoticeList() {
queryParams.pageNum = 1;
fetchNoticeList();
}
// 分页加载列表
function fetchNoticeList() {
loadMoreState.value = "loading";
NoticeAPI.getPage(queryParams)
.then((data) => {
pageData.value = data.list;
total.value = data.total;
queryParams.pageNum++;
})
.catch(() => {
pageData.value = [];
})
.finally(() => {
loadMoreState.value = "finished";
});
}
// 打开详情弹窗
async function openNoticeDetail(item: NoticeItem) {
const detail = await NoticeAPI.getDetail(item.id);
noticeDetail.value = detail;
detailDialog.visible = true;
}
// 关闭详情弹窗
function closeNoticeDetail() {
detailDialog.visible = false;
}
// 打开表单弹窗
async function openNoticeForm(id?: number) {
formRef.value?.reset();
Object.assign(formData, initialFormData);
formDialog.visible = true;
if (id) {
formData.id = id;
const data = await NoticeAPI.getFormData(id);
Object.assign(formData, data, { id });
}
}
// 关闭表单弹窗
function closeNoticeForm() {
formDialog.visible = false;
formRef.value?.reset();
Object.assign(formData, initialFormData);
}
// 提交表单
function submitNoticeForm() {
formRef.value.validate().then(({ valid }: { valid: boolean }) => {
if (!valid) return;
isSubmitting.value = true;
const id = formData.id;
const action = id ? NoticeAPI.update(id, formData) : NoticeAPI.add(formData);
action
.then(() => {
toast.success("操作成功");
closeNoticeForm();
loadNoticeList();
})
.finally(() => {
isSubmitting.value = false;
});
});
}
// 更多操作
const actionSheetVisible = ref(false);
const actionSheetActions = ref<{ name: string; color?: string }[]>([]);
const pendingAction = ref<Record<string, () => void>>({});
function showNoticeActions(item: NoticeItem) {
const actions: { name: string; color?: string }[] = [{ name: "查看" }];
const actionMap: Record<string, () => void> = {
查看: () => openNoticeDetail(item),
};
if (item.publishStatus !== 1) {
if (hasPermission("sys:notice:update")) {
actions.push({ name: "编辑" });
actionMap["编辑"] = () => openNoticeForm(Number(item.id));
}
if (hasPermission("sys:notice:delete")) {
actions.push({ name: "删除", color: "var(--color-danger)" });
actionMap["删除"] = async () => {
try {
await messageBox({
title: "确认删除",
msg: `确定要删除通知「${item.title}」吗?`,
type: "warning",
});
await NoticeAPI.deleteByIds(item.id);
toast.success("删除成功");
loadNoticeList();
} catch {
// 用户取消操作
}
};
}
if (hasPermission("sys:notice:publish")) {
actions.push({ name: "发布" });
actionMap["发布"] = async () => {
try {
await messageBox({
title: "确认发布",
msg: `确定要发布通知「${item.title}」吗?`,
type: "warning",
});
await NoticeAPI.publish(Number(item.id));
toast.success("发布成功");
loadNoticeList();
} catch {
// 用户取消操作
}
};
}
} else {
if (hasPermission("sys:notice:revoke")) {
actions.push({ name: "撤回", color: "var(--color-warning)" });
actionMap["撤回"] = async () => {
try {
await messageBox({
title: "确认撤回",
msg: `确定要撤回通知「${item.title}」吗?`,
type: "warning",
});
await NoticeAPI.revoke(Number(item.id));
toast.success("撤回成功");
loadNoticeList();
} catch {
// 用户取消操作
}
};
}
}
actionSheetActions.value = actions;
pendingAction.value = actionMap;
actionSheetVisible.value = true;
}
function handleActionSelect({ value }: { value: string }) {
pendingAction.value[value]?.();
}
onReachBottom(() => {
if (queryParams.pageNum * queryParams.pageSize < total.value) {
fetchNoticeList();
} else {
loadMoreState.value = "finished";
}
});
onLoad(() => {
loadNoticeList();
});
</script>
<script lang="ts">
export default { options: { styleIsolation: "shared" } };
</script>
<route lang="json">
{
"name": "notice",
"style": {
"navigationBarTitleText": "通知公告"
}
}
</route>
<style lang="scss" scoped>
.notice-card__main {
flex: 1;
}
.notice-card__title {
font-weight: 700;
font-size: 32rpx;
}
.notice-card__publisher {
font-size: 24rpx;
color: var(--color-text-secondary);
}
.notice-card__meta {
display: flex;
gap: 24rpx;
margin-top: 12rpx;
}
.notice-card__detail {
display: flex;
align-items: flex-start;
min-width: 0;
}
.notice-card__detail-text {
margin-left: 8rpx;
font-size: 24rpx;
color: var(--color-text-secondary);
}
.notice-card__footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 16rpx;
}
.notice-card__time {
font-size: 24rpx;
color: var(--color-text-placeholder);
}
.notice-card__action {
display: flex;
align-items: center;
justify-content: center;
width: 64rpx;
height: 64rpx;
border-radius: 50%;
}
.notice-card__action--hover {
background: rgba(var(--color-text-placeholder-rgb, 148, 163, 184), 0.16);
}
</style>