This commit is contained in:
owenchen committed 2026-10-13 14:23:10 +08:00
1 parent 3c0d389fcd
commit 32f237c4bf
22 files changed
+3351 -24

No files matched your search

@@ -0,0 +1,208 @@
<script setup lang="ts">
/**
* 用户查询(列表页):旧 WebVue views/security/UserEnquiry.vue 的移动端等价物
*
* 保留的东西:三条筛选项(用户名/中文名/创建日期区间)、每页 20 条、
* 按创建日期倒序、行点击进详情、多选批量"无效"(旧页面也是循环 Delete,不是 Deletes)。
* 丢掉的东西:多列网格、列配置、打印——手机上没有对应交互。
*
* 页面只组数据与路由,列表/筛选/分页的机制全在 admin-list + useAdminList 里。
*/
import type { AdminColumn, AdminQuery, AdminRow } from "@/admin/admin-types";
import { useLocale } from "uview-pro";
import { computed, nextTick, reactive, ref, watch } from "vue";
import { fillCriteria, makeFilterOption } from "@/admin/admin-filter";
import { useAdminList, useAuthGuard } from "@/composables";
import { SecurityUserEntity } from "@/data/auto/securityUserEntity";
import { SecurityUserSearchCriteria } from "@/data/auto/securityUserSearchCriteria";
import { SearchFilterOperatorPolicy } from "@/data/custom/searchFilterOperatorPolicy";
import { SearchFilterTypePolicy } from "@/data/custom/searchFilterTypePolicy";
import { usePagePermission } from "@/permission/permission-context";
import { pageUrl } from "@/router/routes";
import securityUserService from "@/service/security/securityUserService";
const { t, currentLocale } = useLocale();
const perm = usePagePermission("userEnquiry");
/** 列名 -> i18n key:抽屉标签、卡片行标签都靠它,切换语言时就地刷新而不重建条件 */
const LABEL_KEYS: Record<string, string> = {
[SecurityUserEntity.DB_USER_NAME]: "UserPage.userName",
[SecurityUserEntity.DB_USER_LOCAL_NAME]: "UserPage.userLocalName",
[SecurityUserEntity.DB_CREATED_DATETIME]: "UserPage.createdDatetime",
};
// reactive 是必须的:useAdminList 的 filters 是 computed,源数组非响应式就永远不会失效,
// 改了条件却仍是旧筛选结果是这一层最容易踩的坑
const bindOptions = reactive([
makeFilterOption(
SecurityUserEntity.DB_USER_NAME,
t(LABEL_KEYS[SecurityUserEntity.DB_USER_NAME]),
SearchFilterTypePolicy.TEXT,
SearchFilterOperatorPolicy.CONTAIN,
),
makeFilterOption(
SecurityUserEntity.DB_USER_LOCAL_NAME,
t(LABEL_KEYS[SecurityUserEntity.DB_USER_LOCAL_NAME]),
SearchFilterTypePolicy.TEXT,
SearchFilterOperatorPolicy.CONTAIN,
),
makeFilterOption(
SecurityUserEntity.DB_CREATED_DATETIME,
t(LABEL_KEYS[SecurityUserEntity.DB_CREATED_DATETIME]),
SearchFilterTypePolicy.DATE,
SearchFilterOperatorPolicy.DATE_BETWEEN,
),
]);
/** 取数:criteria 每页重建(与旧实现一致),只在这里碰具体 service */
async function fetchData(query: AdminQuery) {
const criteria = new SecurityUserSearchCriteria();
criteria.GridColumns = [];
fillCriteria(criteria, query.filters, query);
const data = await securityUserService.FindBySearchCriteria(criteria);
return { rows: data?.Datas ?? [], total: data?.Total ?? 0 };
}
const state = useAdminList({
bindOptions,
loader: fetchData,
orderby: SecurityUserEntity.DB_CREATED_DATETIME,
orderbytag: "desc",
});
// 解构出来的是 ref/computed,交给模板自动 .value;方法仍走 state.xxx
const { filterCount, filterVisible, draft, searchToken } = state;
const columns = computed<AdminColumn[]>(() => [
{
label: t("UserPage.userLocalName"),
prop: SecurityUserEntity.USER_LOCAL_NAME,
},
{ label: t("UserPage.userName"), prop: SecurityUserEntity.USER_NAME },
{ label: t("UserPage.userMobile"), prop: SecurityUserEntity.USER_MOBILE },
{
label: t("UserPage.createdDatetime"),
prop: SecurityUserEntity.CREATED_DATETIME,
date: "datetime",
},
]);
const keyword = ref("");
const listRef = ref<any>(null);
/** 顶部搜索框:把关键字并进"用户名包含"这一条条件,复用同一套筛选链路 */
function onSearch(): void {
const option = bindOptions.find(
item => item.BindValue === SecurityUserEntity.DB_USER_NAME,
);
if (option) {
option.Value = keyword.value;
}
state.reload();
}
function onRowTap(row: AdminRow): void {
// 与旧页面一致:id 用 UserName,mode 决定详情页是只读还是编辑
// 不做 encodeURIComponent:见 composables/useAdminPick 的说明(H5 二次编码坑)
uni.navigateTo({
url: `${pageUrl("userOperation")}?id=${row.UserName}&mode=view`,
});
}
function onCreate(): void {
uni.navigateTo({ url: `${pageUrl("userOperation")}?mode=create` });
}
/** 批量作废:旧 UserEnquiry 也是逐条 Delete,Delete 语义最贴近后端 */
async function onDelete(rows: AdminRow[]): Promise<void> {
if (!rows.length) {
uni.showToast({ title: t("AdminPage.pickNoneTip"), icon: "none" });
return;
}
uni.showModal({
title: t("AdminPage.confirmDeleteTitle"),
success: async (res: any) => {
if (!res.confirm) {
return;
}
for (const row of rows) {
await securityUserService.Delete(String(row.UserName));
}
listRef.value?.clearSelection();
state.reload();
uni.showToast({ title: t("AdminPage.deletedTip"), icon: "none" });
},
});
}
// 语言切换后条件标签也要跟着换(值不动,只改显示名)
watch(currentLocale, () => {
for (const option of bindOptions) {
const key = LABEL_KEYS[option.BindValue];
if (key) {
option.DisplayName = t(key);
}
}
});
// 取数排在登录守卫之后:admin-list 的 auto 默认 false,首屏由这里手动触发一次
let booted = false;
useAuthGuard(() => {
if (booted) {
return;
}
booted = true;
nextTick(() => listRef.value?.refresh());
});
</script>
<template>
<view class="page">
<admin-list ref="listRef" v-model:keyword="keyword" :nav-title="t('UserPage.title')" :show-nav-back="false"
:columns="columns" :loader="state.query" :row-key="SecurityUserEntity.USER_NAME" title-key="UserLocalName"
:selectable="Boolean(perm.canDelete.value)" :filter-count="filterCount" :filter-visible="filterVisible"
:filter-items="draft" :search-token="searchToken" @search="onSearch" @open-filter="state.openFilter()"
@close-filter="state.closeFilter()" @apply-filter="state.applyFilter($event)"
@clear-filter="state.clearFiltersAndSearch()" @row-tap="onRowTap">
<template #toolbar>
<view v-if="perm.canCreate.value" class="create-entry" @click="onCreate">
<u-icon name="plus" size="24" color="#ffffff" />
<text class="create-text">
{{ t("AdminPage.createBut") }}
</text>
</view>
</template>
<template #actions="{ rows }">
<u-button v-if="perm.canDelete.value" type="error" shape="circle" size="mini"
:text="t('AdminPage.deleteBut')" @click="onDelete(rows)" />
</template>
</admin-list>
</view>
</template>
<style lang="scss" scoped>
.page {
width: 100%;
height: 100vh;
background-color: $u-bg-color;
}
/* 新增:翠绿实心胶囊,靠右落在次级工具行里 */
.create-entry {
display: flex;
flex-direction: row;
align-items: center;
padding: 12rpx 28rpx;
color: #ffffff;
background-color: $u-type-primary;
border-radius: 999rpx;
box-shadow: 0 6rpx 16rpx rgba(5, 150, 105, 0.24);
}
.create-text {
margin-left: 8rpx;
font-size: 26rpx;
font-weight: 500;
}
</style>