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

398 lines
13 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/ActorOperation.vue 的移动端等价物
*
* 逐条对齐旧逻辑:
* - onLoad 读 id(=ActorName)与 mode(view|create|amend);
* - FindByKey 取 SecurityActorData,取不到提示"记录不存在";
* - 保存按 CreatedBy 有无决定 Update / Insert,成功后回填主键、退回只读;
* - 两个子表:组别明细(SecurityActorGroups,选 group-enquiry 多选)、
* 用户明细(SecurityUserActors,选 user-enquiry 多选),均按主键去重合并、删除即剔除。
*
* 旧页面用 dragModal 内嵌 Enquiry + provide/inject 做选择;移动端改成真跳选择页再回传
* (见 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 { SecurityActorData } from "@/data/auto/securityActorData";
import { SecurityActorGroupViewEntity } from "@/data/auto/securityActorGroupViewEntity";
import { SecurityUserActorViewEntity } from "@/data/auto/securityUserActorViewEntity";
import { usePagePermission } from "@/permission/permission-context";
import { smartBack } from "@/router/routes";
import securityActorService from "@/service/security/securityActorService";
const { t } = useLocale();
const perm = usePagePermission("actorOperation");
const data = ref<SecurityActorData>(new SecurityActorData());
const actor = computed(() => data.value.SecurityActor);
const readonly = ref(true);
const routeId = ref("");
/** 新增态判定:没有 CreatedBy 就说明是还没落库的新记录 */
const isNew = computed(() => !String(actor.value.CreatedBy ?? "").trim());
const heroTitle = computed(
() => String(actor.value.ActorLocalName || actor.value.ActorName || ""),
);
const heroChips = computed(() => {
const chips: string[] = [];
if (actor.value.ActorName) {
chips.push(`${t("ActorPage.actorName")}: ${actor.value.ActorName}`);
}
return chips;
});
const sections = computed<AdminFormSection[]>(() => {
const fields: AdminFormField[] = [
{
label: t("ActorPage.actorName"),
prop: "ActorName",
required: true,
// 已有记录时主键锁死:与旧页面"编辑态 ActorName 只读"一致
lock: routeId.value !== "",
},
{ label: t("ActorPage.actorLocalName"), prop: "ActorLocalName", required: true },
{ label: t("ActorPage.actorEngName"), prop: "ActorEngName" },
];
return [{ title: t("ActorPage.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 securityActorService.FindByKey(routeId.value);
if (!result?.SecurityActor?.ActorName) {
uni.showToast({ title: t("AdminPage.notExistTip"), icon: "none" });
return;
}
data.value = result;
if (!data.value.SecurityActorGroups) {
data.value.SecurityActorGroups = [];
}
if (!data.value.SecurityUserActors) {
data.value.SecurityUserActors = [];
}
}
function onFieldChange(prop: string, value: unknown): void {
(actor.value as Record<string, any>)[prop] = value;
}
async function onSave(): Promise<void> {
if (!String(actor.value.ActorName ?? "").trim()) {
uni.showToast({ title: t("ActorPage.actorNameRequired"), icon: "none" });
return;
}
if (!String(actor.value.ActorLocalName ?? "").trim()) {
uni.showToast({ title: t("ActorPage.localNameRequired"), icon: "none" });
return;
}
if (!data.value.SecurityUserActors) {
data.value.SecurityUserActors = [];
}
if (!data.value.SecurityActorGroups) {
data.value.SecurityActorGroups = [];
}
const isUpdate = !isNew.value;
const result = await (isUpdate
? securityActorService.Update(data.value)
: securityActorService.Insert(data.value));
if (result?.SecurityActor) {
data.value = result;
}
// 服务端保存返回可能不含子表字段:统一兑底成 [],否则模板读 .length 会报 undefined
if (!data.value.SecurityActorGroups) {
data.value.SecurityActorGroups = [];
}
if (!data.value.SecurityUserActors) {
data.value.SecurityUserActors = [];
}
routeId.value = String(actor.value.ActorName ?? "");
readonly.value = true;
uni.showToast({ title: t("AdminPage.savedTip"), icon: "none" });
}
async function onDelete(): Promise<void> {
await securityActorService.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");
}
// 组别选择:跳 group-enquiry(多选),回传后按 GroupName 去重合并
const groupPicker = useAdminPick<SecurityActorGroupViewEntity>("group");
function onAddGroup(): void {
groupPicker.open({ page: "groupEnquiry", params: { multiple: "1" } }, (rows) => {
const exists = new Set(data.value.SecurityActorGroups.map(item => item.GroupName));
for (const row of rows) {
const groupName = String(row.GroupName ?? "");
if (!groupName || exists.has(groupName)) {
continue;
}
const entity = new SecurityActorGroupViewEntity();
entity.ActorName = String(actor.value.ActorName ?? "");
entity.GroupName = groupName;
entity.GroupLocalName = String(row.GroupLocalName ?? "");
data.value.SecurityActorGroups.push(entity);
exists.add(groupName);
}
});
}
function onDeleteGroup(groupName: string): void {
data.value.SecurityActorGroups = data.value.SecurityActorGroups.filter(
item => item.GroupName !== groupName,
);
}
// 用户选择:跳 user-enquiry(多选),回传后按 UserName 去重合并
const userPicker = useAdminPick<SecurityUserActorViewEntity>("user");
function onAddUser(): void {
userPicker.open({ page: "userEnquiry", params: { multiple: "1" } }, (rows) => {
const exists = new Set(data.value.SecurityUserActors.map(item => item.UserName));
for (const row of rows) {
const userName = String(row.UserName ?? "");
if (!userName || exists.has(userName)) {
continue;
}
const entity = new SecurityUserActorViewEntity();
entity.UserName = userName;
entity.UserLocalName = String(row.UserLocalName ?? "");
entity.ActorName = String(actor.value.ActorName ?? "");
data.value.SecurityUserActors.push(entity);
exists.add(userName);
}
});
}
function onDeleteUser(userName: string): void {
data.value.SecurityUserActors = data.value.SecurityUserActors.filter(
item => item.UserName !== userName,
);
}
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('ActorPage.detailTitle')" :show-nav-back="true" :hero-title="heroTitle"
:hero-chips="heroChips" :sections="sections" :model="actor" :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("ActorPage.groupTitle") }}
</text>
<view v-if="!ro && perm.canAmend.value" class="group-add" @click="onAddGroup">
<u-icon name="plus" size="24" color="#059669" />
<text class="group-add-text">
{{ t("ActorPage.addGroupBut") }}
</text>
</view>
</view>
<view class="group-card">
<view v-if="!data.SecurityActorGroups.length" class="group-empty">
<text>
{{ t("ActorPage.groupEmpty") }}
</text>
</view>
<view v-for="item in data.SecurityActorGroups" :key="item.GroupName" class="actor-row">
<view class="actor-text">
<text class="actor-name">
{{ item.GroupLocalName || item.GroupName || t("ActorPage.unnamedGroup") }}
</text>
<text v-if="item.GroupLocalName" class="actor-sub">
{{ item.GroupName }}
</text>
</view>
<u-icon v-if="!ro && perm.canAmend.value" name="trash" size="34" color="#c4c8cf"
@click="onDeleteGroup(item.GroupName)" />
</view>
</view>
</view>
<!-- 用户明细 -->
<view class="group">
<view class="group-head">
<text class="group-title">
{{ t("ActorPage.userTitle") }}
</text>
<view v-if="!ro && perm.canAmend.value" class="group-add" @click="onAddUser">
<u-icon name="plus" size="24" color="#059669" />
<text class="group-add-text">
{{ t("ActorPage.addUserBut") }}
</text>
</view>
</view>
<view class="group-card">
<view v-if="!data.SecurityUserActors.length" class="group-empty">
<text>
{{ t("ActorPage.userEmpty") }}
</text>
</view>
<view v-for="item in data.SecurityUserActors" :key="item.UserName" class="actor-row">
<view class="actor-text">
<text class="actor-name">
{{ item.UserLocalName || item.UserName || t("ActorPage.unnamedUser") }}
</text>
<text v-if="item.UserLocalName" class="actor-sub">
{{ item.UserName }}
</text>
</view>
<u-icon v-if="!ro && perm.canAmend.value" name="trash" size="34" color="#c4c8cf"
@click="onDeleteUser(item.UserName)" />
</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>