ow
This commit is contained in:
1 parent
3c0d389fcd
commit
32f237c4bf
22 files changed
+3351
-24
No files matched your search
@@ -1,33 +1,127 @@
|
||||
<script setup lang="ts">
|
||||
// 登录后的落地页:当前为空白占位,后续由旧 WebVue 项目的
|
||||
// 菜单(/SecurityFunction/FindMenus)+ 权限守卫驱动真实内容
|
||||
import { useAuthGuard } from "@/composables";
|
||||
/**
|
||||
* 登录后的落地页:后端按用户下发的功能菜单驱动入口网格
|
||||
*
|
||||
* 数据链路(详见 composables/useAdminMenu.ts):
|
||||
* /SecurityUser/FindMenuByUsername(按用户授权)-> menu-map.ts 白名单 -> canVisitPage 复核。
|
||||
* 三层过滤后剩下的才是能点的入口,绝不用 FindMenus 全量表兜底(会列出未授权功能)。
|
||||
*/
|
||||
import type { PageName } from "@/router/routes";
|
||||
import { useLocale } from "uview-pro";
|
||||
import { computed } from "vue";
|
||||
import { useAdminMenu, useAuthGuard } from "@/composables";
|
||||
import { WebSitePolicy } from "@/policy/custom/webSitePolicy";
|
||||
import { pageUrl } from "@/router/routes";
|
||||
|
||||
// 冷启动深链 / 分享链接直达本页时不经过任何导航 API,拦截器拦不到,故页面级再复核一次
|
||||
useAuthGuard();
|
||||
const { t } = useLocale();
|
||||
const menu = useAdminMenu();
|
||||
|
||||
/** 可见入口:useAdminMenu 里已做过白名单 + canVisitPage 两道过滤 */
|
||||
const items = computed(() => menu.visible.value);
|
||||
/** 拉取失败(不是"没权限"就是网络/后端异常):给出重试入口而不是空一片 */
|
||||
const failed = computed(() => !menu.loading.value && Boolean(menu.errorText.value));
|
||||
|
||||
function openPage(page: PageName): void {
|
||||
uni.navigateTo({ url: pageUrl(page) });
|
||||
}
|
||||
|
||||
// 菜单拉取排在登录守卫之后;refresh 自带按账号的模块级缓存,反复 onShow 不会重复打接口
|
||||
useAuthGuard(() => {
|
||||
menu.refresh();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<app-page :nav-title="WebSitePolicy.Title">
|
||||
<app-page :nav-title="WebSitePolicy.Title" :show-nav-back="false" :hide-nav="true">
|
||||
<view class="dashboard">
|
||||
<view class="dashboard__placeholder">
|
||||
{{ WebSitePolicy.Title }}
|
||||
<view v-if="menu.loading.value" class="dashboard__tip">
|
||||
<text>{{ t("AdminPage.menuLoading") }}</text>
|
||||
</view>
|
||||
|
||||
<view v-else-if="failed" class="dashboard__tip">
|
||||
<text>{{ t("AdminPage.noPermissionText") }}</text>
|
||||
<u-button class="dashboard__retry" shape="circle" size="mini" :text="t('AdminPage.menuRetryBut')"
|
||||
@click="menu.refresh(true)" />
|
||||
</view>
|
||||
|
||||
<scroll-view v-else class="dashboard__scroll" scroll-y :show-scrollbar="false">
|
||||
<view v-if="!items.length" class="dashboard__empty">
|
||||
<u-empty mode="list" :text="t('AdminPage.menuEmptyText')" />
|
||||
</view>
|
||||
|
||||
<view v-else class="grid">
|
||||
<view v-for="item in items" :key="item.function" class="grid-item" @click="openPage(item.page)">
|
||||
<view class="grid-icon">
|
||||
<u-icon :name="item.icon" size="46" color="var(--u-type-primary)" />
|
||||
</view>
|
||||
<text class="grid-title">
|
||||
{{ item.title }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</app-page>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dashboard {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 60vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 60vh;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.dashboard__placeholder {
|
||||
font-size: 32rpx;
|
||||
color: $u-tips-color;
|
||||
.dashboard__tip {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 80rpx 0;
|
||||
font-size: 26rpx;
|
||||
color: $u-tips-color;
|
||||
}
|
||||
|
||||
.dashboard__retry {
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
.dashboard__scroll {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.dashboard__empty {
|
||||
padding: 80rpx 0;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.grid-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 33.33%;
|
||||
padding: 28rpx 0;
|
||||
}
|
||||
|
||||
.grid-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
margin-bottom: 12rpx;
|
||||
background-color: #ffffff;
|
||||
border-radius: 24rpx;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.grid-title {
|
||||
font-size: 24rpx;
|
||||
color: $u-content-color;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,121 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 角色查询(ActorEnquiry):旧 WebVue views/security/ActorEnquiry.vue 的移动端等价物
|
||||
*
|
||||
* 它有两种打开方式,同一套列表:
|
||||
* 1. 从菜单进(无 channel):普通查询页,点行看详情(本批暂未接详情,仅回传/返回);
|
||||
* 2. 被 user-operation 当"角色选择器"跳进来(带 ?channel=&multiple=1):
|
||||
* 选完 uni.$emit 回传再 navigateBack(见 useAdminPick / useAdminPickTarget 协议)。
|
||||
*
|
||||
* 旧系统里这个选择能力是 Operation 用 <dragModal> 内嵌 Enquiry + provide/inject 实现的;
|
||||
* 移动端页面之间没有 provide/inject,只能真跳一页再回传,所以这个页面本身就是 Enquiry。
|
||||
*/
|
||||
import type { AdminColumn, AdminQuery, AdminRow } from "@/admin/admin-types";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { useLocale } from "uview-pro";
|
||||
import { computed, nextTick, reactive, ref } from "vue";
|
||||
import { fillCriteria, makeFilterOption } from "@/admin/admin-filter";
|
||||
import { useAdminList, useAdminPickTarget, useAuthGuard } from "@/composables";
|
||||
import { SecurityActorEntity } from "@/data/auto/securityActorEntity";
|
||||
import { SecurityActorSearchCriteria } from "@/data/auto/securityActorSearchCriteria";
|
||||
import { SearchFilterOperatorPolicy } from "@/data/custom/searchFilterOperatorPolicy";
|
||||
import { SearchFilterTypePolicy } from "@/data/custom/searchFilterTypePolicy";
|
||||
import securityActorService from "@/service/security/securityActorService";
|
||||
|
||||
const { t } = useLocale();
|
||||
const pickTarget = useAdminPickTarget();
|
||||
|
||||
const bindOptions = reactive([
|
||||
makeFilterOption(
|
||||
SecurityActorEntity.DB_ACTOR_LOCAL_NAME,
|
||||
t("UserPage.actorLocalName"),
|
||||
SearchFilterTypePolicy.TEXT,
|
||||
SearchFilterOperatorPolicy.CONTAIN,
|
||||
),
|
||||
]);
|
||||
|
||||
async function fetchData(query: AdminQuery) {
|
||||
const criteria = new SecurityActorSearchCriteria();
|
||||
criteria.GridColumns = [];
|
||||
fillCriteria(criteria, query.filters, query);
|
||||
const data = await securityActorService.FindBySearchCriteria(criteria);
|
||||
return { rows: data?.Datas ?? [], total: data?.Total ?? 0 };
|
||||
}
|
||||
|
||||
const state = useAdminList({ bindOptions, loader: fetchData });
|
||||
const { filterCount, filterVisible, draft, searchToken } = state;
|
||||
|
||||
const columns = computed<AdminColumn[]>(() => [
|
||||
{
|
||||
label: t("UserPage.actorLocalName"),
|
||||
prop: SecurityActorEntity.ACTOR_LOCAL_NAME,
|
||||
},
|
||||
{ label: t("UserPage.actorName"), prop: SecurityActorEntity.ACTOR_NAME },
|
||||
]);
|
||||
|
||||
const keyword = ref("");
|
||||
const listRef = ref<any>(null);
|
||||
|
||||
function onSearch(): void {
|
||||
const option = bindOptions.find(
|
||||
item => item.BindValue === SecurityActorEntity.DB_ACTOR_LOCAL_NAME,
|
||||
);
|
||||
if (option) {
|
||||
option.Value = keyword.value;
|
||||
}
|
||||
state.reload();
|
||||
}
|
||||
|
||||
function onRowTap(row: AdminRow): void {
|
||||
// 多选态交给勾选,不在这里回传;单选态点一行即选中
|
||||
if (pickTarget.multiple.value) {
|
||||
return;
|
||||
}
|
||||
pickTarget.deliverOne(row);
|
||||
}
|
||||
|
||||
function onConfirm(rows: AdminRow[]): void {
|
||||
if (!rows.length) {
|
||||
uni.showToast({ title: t("AdminPage.pickNoneTip"), icon: "none" });
|
||||
return;
|
||||
}
|
||||
pickTarget.deliver(rows);
|
||||
}
|
||||
|
||||
onLoad((options) => {
|
||||
pickTarget.readOptions(options);
|
||||
});
|
||||
|
||||
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.actorEnquiryTitle')"
|
||||
:columns="columns" :loader="state.query" :row-key="SecurityActorEntity.ACTOR_NAME"
|
||||
title-key="ActorLocalName" :selectable="pickTarget.multiple.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 #actions="{ rows }">
|
||||
<u-button type="primary" shape="circle" size="mini" :text="t('AdminPage.pickDoneBut')"
|
||||
@click="onConfirm(rows)" />
|
||||
</template>
|
||||
</admin-list>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background-color: $u-bg-color;
|
||||
}
|
||||
</style>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,338 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 用户详情(查看 / 新增 / 修改):旧 WebVue views/security/UserOperation.vue 的移动端等价物
|
||||
*
|
||||
* 逐条对齐旧逻辑:
|
||||
* - onLoad 读 id(=UserName)与 mode(view|create|amend);
|
||||
* - FindByKey 取 SecurityUserData,取不到提示"记录不存在";
|
||||
* - 保存按 CreatedBy 有无决定 Update / Insert,成功后把主键回填、退回只读;
|
||||
* - 密码编辑的是 SecurityUserDisplayData.OrgUserPassword(留空即不改);
|
||||
* - 角色明细按 ActorName 去重合并,删除即从数组里剔除。
|
||||
*
|
||||
* 旧页面的"选角色"是弹窗内嵌 Enquiry + provide/inject;这里改成真跳 actor-enquiry 页
|
||||
* 再回传(见 composables/useAdminPick),所以角色区放在 admin-form 的 #extra 插槽里自绘。
|
||||
*/
|
||||
import type {
|
||||
AdminFormAction,
|
||||
AdminFormField,
|
||||
AdminFormSection,
|
||||
} from "@/admin/admin-types";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { useLocale } from "uview-pro";
|
||||
import { computed, ref } from "vue";
|
||||
import { useAdminPick, useAuthGuard } from "@/composables";
|
||||
import { SecurityUserActorViewEntity } from "@/data/auto/securityUserActorViewEntity";
|
||||
import { SecurityUserData } from "@/data/auto/securityUserData";
|
||||
import { usePagePermission } from "@/permission/permission-context";
|
||||
import securityUserService from "@/service/security/securityUserService";
|
||||
|
||||
const { t } = useLocale();
|
||||
const perm = usePagePermission("userOperation");
|
||||
|
||||
const data = ref<SecurityUserData>(new SecurityUserData());
|
||||
const user = computed(() => data.value.SecurityUser);
|
||||
const readonly = ref(true);
|
||||
const routeId = ref("");
|
||||
|
||||
/** 新增态判定:没有 CreatedBy 就说明是还没落库的新记录 */
|
||||
const isNew = computed(() => !String(user.value.CreatedBy ?? "").trim());
|
||||
|
||||
const heroTitle = computed(
|
||||
() => String(user.value.UserLocalName || user.value.UserName || ""),
|
||||
);
|
||||
const heroChips = computed(() => {
|
||||
const chips: string[] = [];
|
||||
if (user.value.UserName) {
|
||||
chips.push(`${t("UserPage.account")}: ${user.value.UserName}`);
|
||||
}
|
||||
if (user.value.UserMobile) {
|
||||
chips.push(`${t("UserPage.userMobile")}: ${user.value.UserMobile}`);
|
||||
}
|
||||
return chips;
|
||||
});
|
||||
|
||||
const sections = computed<AdminFormSection[]>(() => {
|
||||
const fields: AdminFormField[] = [
|
||||
{
|
||||
label: t("UserPage.userName"),
|
||||
prop: "UserName",
|
||||
required: true,
|
||||
// 已有记录时主键锁死:与旧页面"编辑态 UserName 只读"一致
|
||||
lock: routeId.value !== "",
|
||||
},
|
||||
{ label: t("UserPage.userLocalName"), prop: "UserLocalName", required: true },
|
||||
{ label: t("UserPage.userEngName"), prop: "UserEngName" },
|
||||
{ label: t("UserPage.userMobile"), prop: "UserMobile" },
|
||||
{ label: t("UserPage.createdDatetime"), prop: "CreatedDatetime", date: "datetime", lock: true },
|
||||
];
|
||||
// 密码只在编辑态出现:查看态没必要显示(即便是掩码)
|
||||
if (!readonly.value) {
|
||||
fields.push({
|
||||
label: t("UserPage.userPassword"),
|
||||
prop: "OrgUserPassword",
|
||||
type: "password",
|
||||
placeholder: t("UserPage.passwordTip"),
|
||||
});
|
||||
}
|
||||
return [{ title: t("UserPage.detailTitle"), fields }];
|
||||
});
|
||||
|
||||
const actions = computed<AdminFormAction[]>(() => {
|
||||
const list: AdminFormAction[] = [];
|
||||
if (perm.canAmend.value) {
|
||||
list.push({ key: "amend", label: t("AdminPage.amendBut"), when: "view" });
|
||||
}
|
||||
if (perm.canDelete.value) {
|
||||
list.push({
|
||||
key: "delete",
|
||||
label: t("AdminPage.deleteBut"),
|
||||
tone: "error",
|
||||
confirm: true,
|
||||
when: "view",
|
||||
});
|
||||
}
|
||||
list.push({ key: "save", label: t("AdminPage.saveBut"), when: "edit" });
|
||||
list.push({ key: "cancel", label: t("AdminPage.cancelBut"), when: "edit" });
|
||||
return list;
|
||||
});
|
||||
|
||||
async function load(): Promise<void> {
|
||||
const result = await securityUserService.FindByKey(routeId.value);
|
||||
if (!result?.SecurityUser) {
|
||||
uni.showToast({ title: t("AdminPage.notExistTip"), icon: "none" });
|
||||
return;
|
||||
}
|
||||
data.value = result;
|
||||
}
|
||||
|
||||
/** 字段改写:admin-form 不直接改 props.model,统一回抛到这里 */
|
||||
function onFieldChange(prop: string, value: unknown): void {
|
||||
(user.value as Record<string, any>)[prop] = value;
|
||||
}
|
||||
|
||||
async function onSave(): Promise<void> {
|
||||
if (!String(user.value.UserName ?? "").trim()) {
|
||||
uni.showToast({ title: t("UserPage.userNameRequired"), icon: "none" });
|
||||
return;
|
||||
}
|
||||
if (!String(user.value.UserLocalName ?? "").trim()) {
|
||||
uni.showToast({ title: t("UserPage.localNameRequired"), icon: "none" });
|
||||
return;
|
||||
}
|
||||
const isUpdate = !isNew.value;
|
||||
const result = await (isUpdate
|
||||
? securityUserService.Update(data.value)
|
||||
: securityUserService.Insert(data.value));
|
||||
// 后端回写后的档案才是准的(新记录拿到了 CreatedBy)
|
||||
if (result?.SecurityUser) {
|
||||
data.value = result;
|
||||
}
|
||||
routeId.value = String(user.value.UserName ?? "");
|
||||
readonly.value = true;
|
||||
uni.showToast({ title: t("AdminPage.savedTip"), icon: "none" });
|
||||
}
|
||||
|
||||
async function onDelete(): Promise<void> {
|
||||
await securityUserService.Delete(routeId.value);
|
||||
uni.showToast({ title: t("AdminPage.deletedTip"), icon: "none" });
|
||||
setTimeout(() => uni.navigateBack(), 600);
|
||||
}
|
||||
|
||||
function onAction(action: AdminFormAction): void {
|
||||
switch (action.key) {
|
||||
case "amend":
|
||||
readonly.value = false;
|
||||
break;
|
||||
case "save":
|
||||
onSave();
|
||||
break;
|
||||
case "delete":
|
||||
onDelete();
|
||||
break;
|
||||
case "cancel":
|
||||
if (isNew.value || !routeId.value) {
|
||||
uni.navigateBack();
|
||||
return;
|
||||
}
|
||||
readonly.value = true;
|
||||
load();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function onBack(): void {
|
||||
uni.navigateBack();
|
||||
}
|
||||
|
||||
// 角色选择:跳 actor-enquiry(可多选),回传后按 ActorName 去重合并
|
||||
const actorPicker = useAdminPick<SecurityUserActorViewEntity>("actor");
|
||||
|
||||
function onAddActor(): void {
|
||||
actorPicker.open(
|
||||
{ page: "actorEnquiry", params: { multiple: "1" } },
|
||||
(rows) => {
|
||||
const exists = new Set(
|
||||
data.value.SecurityUserActors.map(item => item.ActorName),
|
||||
);
|
||||
for (const row of rows) {
|
||||
const actorName = String(row.ActorName ?? "");
|
||||
if (!actorName || exists.has(actorName)) {
|
||||
continue;
|
||||
}
|
||||
const entity = new SecurityUserActorViewEntity();
|
||||
entity.UserName = String(user.value.UserName ?? "");
|
||||
entity.ActorName = actorName;
|
||||
entity.ActorLocalName = String(row.ActorLocalName ?? "");
|
||||
data.value.SecurityUserActors.push(entity);
|
||||
exists.add(actorName);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function onDeleteActor(actorName: string): void {
|
||||
data.value.SecurityUserActors = data.value.SecurityUserActors.filter(
|
||||
item => item.ActorName !== actorName,
|
||||
);
|
||||
}
|
||||
|
||||
onLoad((options) => {
|
||||
routeId.value = String(options?.id ?? "");
|
||||
const mode = String(options?.mode ?? "view");
|
||||
readonly.value = mode !== "create" && mode !== "amend";
|
||||
});
|
||||
|
||||
// 详情取数排在登录守卫之后,且只跑一次
|
||||
let booted = false;
|
||||
useAuthGuard(() => {
|
||||
if (booted) {
|
||||
return;
|
||||
}
|
||||
booted = true;
|
||||
if (routeId.value) {
|
||||
load();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<admin-form :nav-title="t('UserPage.detailTitle')" :show-nav-back="true" :hero-title="heroTitle"
|
||||
:hero-chips="heroChips" :sections="sections" :model="user" :readonly="readonly" :actions="actions"
|
||||
@back="onBack" @action="onAction" @field-change="onFieldChange">
|
||||
<template #extra="{ readonly: ro }">
|
||||
<view class="group">
|
||||
<view class="group-head">
|
||||
<text class="group-title">
|
||||
{{ t("UserPage.actorTitle") }}
|
||||
</text>
|
||||
<view v-if="!ro && perm.canAmend.value" class="group-add" @click="onAddActor">
|
||||
<u-icon name="plus" size="24" color="#059669" />
|
||||
<text class="group-add-text">
|
||||
{{ t("UserPage.addActorBut") }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="group-card">
|
||||
<view v-if="!data.SecurityUserActors.length" class="group-empty">
|
||||
<text>
|
||||
{{ t("UserPage.actorEmpty") }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view v-for="actor in data.SecurityUserActors" :key="actor.ActorName" class="actor-row">
|
||||
<view class="actor-text">
|
||||
<text class="actor-name">
|
||||
{{ actor.ActorLocalName || actor.ActorName || t("UserPage.unnamedActor") }}
|
||||
</text>
|
||||
<text v-if="actor.ActorLocalName" class="actor-sub">
|
||||
{{ actor.ActorName }}
|
||||
</text>
|
||||
</view>
|
||||
<u-icon v-if="!ro && perm.canAmend.value" name="trash" size="34" color="#c4c8cf"
|
||||
@click="onDeleteActor(actor.ActorName)" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</admin-form>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 角色明细:与 admin-form 的分组卡同一套视觉(细标题 + 白卡 + 行间细线) */
|
||||
.group {
|
||||
margin-bottom: 28rpx;
|
||||
}
|
||||
|
||||
.group-head {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 12rpx 12rpx;
|
||||
}
|
||||
|
||||
.group-title {
|
||||
font-size: 24rpx;
|
||||
color: $u-tips-color;
|
||||
}
|
||||
|
||||
.group-add {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.group-add-text {
|
||||
margin-left: 4rpx;
|
||||
font-size: 24rpx;
|
||||
color: $u-type-primary;
|
||||
}
|
||||
|
||||
.group-card {
|
||||
padding: 0 28rpx;
|
||||
overflow: hidden;
|
||||
background-color: #ffffff;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
.group-empty {
|
||||
padding: 40rpx 0;
|
||||
font-size: 24rpx;
|
||||
color: $u-tips-color;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.actor-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 92rpx;
|
||||
padding: 16rpx 0;
|
||||
border-bottom: 1rpx solid $u-border-color;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.actor-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.actor-name {
|
||||
font-size: 28rpx;
|
||||
color: $u-main-color;
|
||||
}
|
||||
|
||||
.actor-sub {
|
||||
margin-top: 4rpx;
|
||||
font-size: 22rpx;
|
||||
color: $u-tips-color;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user