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,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>