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

+109 -15
View File
@@ -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>