Files
Docker/uniapp/youjiala/src/pages/security/user-operation.vue
T
2026-10-15 19:26:18 +08:00

347 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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 { smartBack } from "@/router/routes";
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;
// 子表数组统一兑底:服务端 FindByKey/保存返回可能缺字段,否则模板读 .length 会报 undefined
if (!data.value.SecurityUserActors) {
data.value.SecurityUserActors = [];
}
}
/** 字段改写: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;
}
if (!data.value.SecurityUserActors) {
data.value.SecurityUserActors = [];
}
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(() => smartBack("mineTab"), 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) {
smartBack("mineTab");
return;
}
readonly.value = true;
load();
break;
default:
break;
}
}
function onBack(): void {
smartBack("mineTab");
}
// 角色选择:跳 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>