feat(release): close handler and microphone gaps
This commit is contained in:
@@ -26,6 +26,26 @@ export type DirectAdminPage = {
|
||||
rows: DirectAdminFeedback[];
|
||||
};
|
||||
|
||||
export type DirectHandlerUser = {
|
||||
userId: number;
|
||||
userName: string;
|
||||
nickName?: string;
|
||||
};
|
||||
|
||||
export type DirectHandlerChannel = {
|
||||
channelCode: DirectChannelCode;
|
||||
channelName: string;
|
||||
roleKey: string;
|
||||
roleAvailable: boolean;
|
||||
users: DirectHandlerUser[];
|
||||
};
|
||||
|
||||
export type DirectHandlerConfig = {
|
||||
tenantId: string;
|
||||
minimumHandlers: number;
|
||||
channels: DirectHandlerChannel[];
|
||||
};
|
||||
|
||||
export const listDirectAdminFeedback = (params: {
|
||||
pageNum: number;
|
||||
pageSize: number;
|
||||
@@ -48,3 +68,39 @@ export const replyDirectFeedback = (
|
||||
data,
|
||||
headers: { repeatSubmit: false }
|
||||
});
|
||||
|
||||
export const getDirectHandlerConfig = (): Promise<ApiResult<DirectHandlerConfig>> =>
|
||||
request({
|
||||
url: '/api/aihr/direct/admin/handlers',
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
export const listDirectHandlerCandidates = (
|
||||
channelCode: DirectChannelCode,
|
||||
keyword = ''
|
||||
): Promise<ApiResult<DirectHandlerUser[]>> =>
|
||||
request({
|
||||
url: `/api/aihr/direct/admin/handlers/${channelCode}/candidates`,
|
||||
method: 'get',
|
||||
params: { keyword }
|
||||
});
|
||||
|
||||
export const assignDirectHandlers = (
|
||||
channelCode: DirectChannelCode,
|
||||
data: { userIds: number[]; expectedTenantId: string }
|
||||
): Promise<ApiResult<DirectHandlerConfig>> =>
|
||||
request({
|
||||
url: `/api/aihr/direct/admin/handlers/${channelCode}`,
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
|
||||
export const removeDirectHandler = (
|
||||
channelCode: DirectChannelCode,
|
||||
data: { userId: number; expectedTenantId: string }
|
||||
): Promise<ApiResult<DirectHandlerConfig>> =>
|
||||
request({
|
||||
url: `/api/aihr/direct/admin/handlers/${channelCode}/remove`,
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
|
||||
@@ -14,7 +14,9 @@ describe('operations sidebar navigation contract', () => {
|
||||
expect(sidebar).toContain('<router-link');
|
||||
expect(sidebar).toContain('canAccessOperations');
|
||||
expect(sidebar).toContain('operationsConsoleRoles');
|
||||
expect(router).toContain("export { operationsConsoleRoles, tenantGovernanceRoles } from './access';");
|
||||
expect(router).toContain(
|
||||
"export { contentConsoleRoles, directHandlerRoles, operationsConsoleRoles, tenantGovernanceRoles } from './access';"
|
||||
);
|
||||
expect(router).toContain('roles: operationsConsoleRoles');
|
||||
expect(guard).toContain('canAccessRoute(to)');
|
||||
expect(sidebar).toContain('aria-labelledby');
|
||||
|
||||
@@ -16,6 +16,11 @@ describe('直达反馈管理端契约', () => {
|
||||
expect(page).toContain('senderDisplayName');
|
||||
expect(page).toContain('匿名反馈不会显示提交人');
|
||||
expect(page).toContain('回复后不可覆盖');
|
||||
expect(page).toContain('五通道处理人配置');
|
||||
expect(api).toContain('/api/aihr/direct/admin/handlers');
|
||||
expect(page).toContain('移动端员工账号和超级管理员不会进入候选');
|
||||
expect(page).toContain('assignDirectHandlers');
|
||||
expect(page).toContain('removeDirectHandler');
|
||||
expect(access).toContain('direct_audit');
|
||||
expect(router).toContain("path: 'direct'");
|
||||
expect(router).toContain('roles: directHandlerRoles');
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
directHandlerDefinitions,
|
||||
directHandlerReadinessText,
|
||||
directHandlerReady,
|
||||
minimumDirectHandlers,
|
||||
normalizeDirectHandlerMinimum
|
||||
} from './direct-handler-readiness';
|
||||
|
||||
describe('五通道处理人就绪规则', () => {
|
||||
it('固定五个业务通道且每个通道至少需要主处理人和替补', () => {
|
||||
expect(directHandlerDefinitions.map((item) => item.roleKey)).toEqual([
|
||||
'direct_president',
|
||||
'direct_finance',
|
||||
'direct_hr',
|
||||
'direct_audit',
|
||||
'direct_operations'
|
||||
]);
|
||||
expect(minimumDirectHandlers).toBe(2);
|
||||
});
|
||||
|
||||
it('少于两名有效处理人时明确提示缺口', () => {
|
||||
expect(directHandlerReady(0)).toBe(false);
|
||||
expect(directHandlerReadinessText(0)).toBe('尚未配置');
|
||||
expect(directHandlerReadinessText(1)).toBe('还差 1 人');
|
||||
expect(directHandlerReady(2)).toBe(true);
|
||||
expect(directHandlerReadinessText(3)).toBe('已配置 3 人');
|
||||
});
|
||||
|
||||
it('以后端返回的最低人数为准,异常值回退到默认规则', () => {
|
||||
expect(directHandlerReady(2, 3)).toBe(false);
|
||||
expect(directHandlerReadinessText(2, 3)).toBe('还差 1 人');
|
||||
expect(normalizeDirectHandlerMinimum(3)).toBe(3);
|
||||
expect(normalizeDirectHandlerMinimum(0)).toBe(minimumDirectHandlers);
|
||||
expect(normalizeDirectHandlerMinimum(undefined)).toBe(minimumDirectHandlers);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
export const minimumDirectHandlers = 2;
|
||||
|
||||
export const directHandlerDefinitions = [
|
||||
{ channelCode: 'PRESIDENT', channelName: '总裁直达', roleKey: 'direct_president' },
|
||||
{ channelCode: 'FINANCE', channelName: '财务直达', roleKey: 'direct_finance' },
|
||||
{ channelCode: 'HR', channelName: '人力直达', roleKey: 'direct_hr' },
|
||||
{ channelCode: 'AUDIT', channelName: '审计直达', roleKey: 'direct_audit' },
|
||||
{ channelCode: 'OPERATIONS', channelName: '运营直达', roleKey: 'direct_operations' }
|
||||
] as const;
|
||||
|
||||
export type DirectHandlerDefinition = (typeof directHandlerDefinitions)[number];
|
||||
|
||||
export const normalizeDirectHandlerMinimum = (minimumHandlers: unknown): number =>
|
||||
typeof minimumHandlers === 'number' && Number.isInteger(minimumHandlers) && minimumHandlers > 0
|
||||
? minimumHandlers
|
||||
: minimumDirectHandlers;
|
||||
|
||||
export const directHandlerReady = (
|
||||
activeHandlerCount: number,
|
||||
minimumHandlers: number = minimumDirectHandlers
|
||||
): boolean => activeHandlerCount >= normalizeDirectHandlerMinimum(minimumHandlers);
|
||||
|
||||
export const directHandlerReadinessText = (
|
||||
activeHandlerCount: number,
|
||||
minimumHandlers: number = minimumDirectHandlers
|
||||
): string => {
|
||||
const requiredHandlers = normalizeDirectHandlerMinimum(minimumHandlers);
|
||||
if (directHandlerReady(activeHandlerCount, requiredHandlers)) return `已配置 ${activeHandlerCount} 人`;
|
||||
return activeHandlerCount === 0
|
||||
? '尚未配置'
|
||||
: `还差 ${requiredHandlers - activeHandlerCount} 人`;
|
||||
};
|
||||
@@ -15,6 +15,104 @@
|
||||
title="每条反馈只允许一条正式回复。回复后不可覆盖,避免处理口径被静默修改。"
|
||||
/>
|
||||
|
||||
<section v-if="isSuperAdmin" class="handler-panel panel">
|
||||
<div class="handler-head">
|
||||
<div>
|
||||
<h2>五通道处理人配置</h2>
|
||||
<p>每个通道至少配置 {{ handlerMinimumHandlers }} 名有效后台用户,分别承担主处理和替补;配置角色不会自动产生回复或工单。</p>
|
||||
</div>
|
||||
<el-button :icon="Refresh" :loading="handlerLoading" @click="loadHandlerConfig">刷新配置</el-button>
|
||||
</div>
|
||||
<el-alert
|
||||
:type="allHandlersReady ? 'success' : 'warning'"
|
||||
:closable="false"
|
||||
show-icon
|
||||
:title="allHandlersReady ? `五个通道均已配置至少 ${handlerMinimumHandlers} 名有效处理人` : `${readyHandlerChannelCount}/5 个通道达到处理人配置要求`"
|
||||
/>
|
||||
<div v-loading="handlerLoading" class="handler-grid">
|
||||
<article v-for="item in handlerConfigs" :key="item.roleKey" class="handler-card">
|
||||
<div class="handler-card-head">
|
||||
<div>
|
||||
<strong>{{ item.channelName }}</strong>
|
||||
<small>{{ item.roleKey }}</small>
|
||||
</div>
|
||||
<el-tag :type="directHandlerReady(item.users.length, handlerMinimumHandlers) ? 'success' : 'danger'">
|
||||
{{ directHandlerReadinessText(item.users.length, handlerMinimumHandlers) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div v-if="item.users.length" class="handler-users">
|
||||
<div v-for="user in item.users" :key="user.userId" class="handler-user">
|
||||
<span>{{ user.nickName || user.userName }}</span>
|
||||
<small>{{ user.userName }}</small>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
:disabled="handlerMutating"
|
||||
@click="removeHandler(item, user)"
|
||||
>
|
||||
解除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="handler-empty">尚无有效处理人,员工提交后将无人接收。</div>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
:disabled="!item.roleAvailable || handlerMutating"
|
||||
@click="openHandlerSelector(item)"
|
||||
>
|
||||
添加处理人
|
||||
</el-button>
|
||||
<small v-if="!item.roleAvailable" class="handler-role-missing">角色不存在或已停用,请先检查系统角色。</small>
|
||||
</article>
|
||||
</div>
|
||||
<el-dialog
|
||||
v-model="handlerDialogVisible"
|
||||
:title="selectedHandlerChannel ? `添加${selectedHandlerChannel.channelName}处理人` : '添加处理人'"
|
||||
width="620px"
|
||||
destroy-on-close
|
||||
>
|
||||
<div class="candidate-toolbar">
|
||||
<el-input
|
||||
v-model="candidateKeyword"
|
||||
clearable
|
||||
placeholder="按用户名或昵称搜索后台用户"
|
||||
@keyup.enter="loadHandlerCandidates"
|
||||
/>
|
||||
<el-button :loading="candidateLoading" @click="loadHandlerCandidates">搜索</el-button>
|
||||
</div>
|
||||
<el-alert
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="仅列出当前租户已启用的后台用户;移动端员工账号和超级管理员不会进入候选。"
|
||||
/>
|
||||
<el-checkbox-group v-model="selectedCandidateUserIds" class="candidate-list">
|
||||
<el-checkbox
|
||||
v-for="candidate in handlerCandidates"
|
||||
:key="candidate.userId"
|
||||
:value="candidate.userId"
|
||||
border
|
||||
>
|
||||
{{ candidate.nickName || candidate.userName }}
|
||||
<small>{{ candidate.userName }}</small>
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
<el-empty v-if="!candidateLoading && !handlerCandidates.length" description="暂无可添加的后台用户" />
|
||||
<template #footer>
|
||||
<el-button @click="handlerDialogVisible = false">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="handlerMutating"
|
||||
:disabled="!selectedCandidateUserIds.length"
|
||||
@click="assignSelectedHandlers"
|
||||
>
|
||||
确认添加
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</section>
|
||||
|
||||
<section class="filter-panel panel">
|
||||
<el-select v-model="channelCode" clearable placeholder="全部授权通道" @change="resetAndLoad">
|
||||
<el-option v-for="item in channels" :key="item.value" :label="item.label" :value="item.value" />
|
||||
@@ -104,14 +202,31 @@
|
||||
<script setup name="ContentDirect" lang="ts">
|
||||
import { Refresh } from '@element-plus/icons-vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import {
|
||||
assignDirectHandlers,
|
||||
getDirectHandlerConfig,
|
||||
listDirectHandlerCandidates,
|
||||
listDirectAdminFeedback,
|
||||
removeDirectHandler,
|
||||
replyDirectFeedback,
|
||||
type DirectAdminFeedback,
|
||||
type DirectChannelCode,
|
||||
type DirectFeedbackStatus
|
||||
type DirectFeedbackStatus,
|
||||
type DirectHandlerChannel,
|
||||
type DirectHandlerConfig as DirectHandlerConfigResponse,
|
||||
type DirectHandlerUser
|
||||
} from '@/api/aihr/direct';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
import {
|
||||
directHandlerDefinitions,
|
||||
directHandlerReadinessText,
|
||||
directHandlerReady,
|
||||
minimumDirectHandlers,
|
||||
normalizeDirectHandlerMinimum
|
||||
} from '@/utils/direct-handler-readiness';
|
||||
|
||||
type DirectHandlerConfig = DirectHandlerChannel;
|
||||
|
||||
const channels: Array<{ value: DirectChannelCode; label: string }> = [
|
||||
{ value: 'PRESIDENT', label: '总裁直达' },
|
||||
@@ -132,9 +247,127 @@ const drawerVisible = ref(false);
|
||||
const selected = ref<DirectAdminFeedback>();
|
||||
const replyContent = ref('');
|
||||
const replying = ref(false);
|
||||
const userStore = useUserStore();
|
||||
const isSuperAdmin = computed(() => userStore.roles.includes('superadmin'));
|
||||
const handlerLoading = ref(false);
|
||||
const handlerMutating = ref(false);
|
||||
const handlerTenantId = ref('');
|
||||
const handlerMinimumHandlers = ref(minimumDirectHandlers);
|
||||
const handlerDialogVisible = ref(false);
|
||||
const selectedHandlerChannel = ref<DirectHandlerConfig>();
|
||||
const candidateKeyword = ref('');
|
||||
const candidateLoading = ref(false);
|
||||
const handlerCandidates = ref<DirectHandlerUser[]>([]);
|
||||
const selectedCandidateUserIds = ref<number[]>([]);
|
||||
const handlerConfigs = ref<DirectHandlerConfig[]>(
|
||||
directHandlerDefinitions.map((item) => ({ ...item, roleAvailable: false, users: [] }))
|
||||
);
|
||||
const readyHandlerChannelCount = computed(() =>
|
||||
handlerConfigs.value.filter((item) =>
|
||||
directHandlerReady(item.users.length, handlerMinimumHandlers.value)
|
||||
).length
|
||||
);
|
||||
const allHandlersReady = computed(() => readyHandlerChannelCount.value === directHandlerDefinitions.length);
|
||||
|
||||
const displayTime = (value?: string) => value ? value.replace('T', ' ').slice(0, 16) : '—';
|
||||
|
||||
const applyHandlerConfig = (config: DirectHandlerConfigResponse) => {
|
||||
handlerTenantId.value = config.tenantId;
|
||||
handlerMinimumHandlers.value = normalizeDirectHandlerMinimum(config.minimumHandlers);
|
||||
handlerConfigs.value = config.channels || [];
|
||||
};
|
||||
|
||||
const loadHandlerConfig = async () => {
|
||||
if (!isSuperAdmin.value || handlerLoading.value) return;
|
||||
handlerLoading.value = true;
|
||||
try {
|
||||
const result = await getDirectHandlerConfig();
|
||||
applyHandlerConfig(result.data);
|
||||
} catch {
|
||||
handlerTenantId.value = '';
|
||||
handlerMinimumHandlers.value = minimumDirectHandlers;
|
||||
handlerConfigs.value = directHandlerDefinitions.map((item) => ({ ...item, roleAvailable: false, users: [] }));
|
||||
ElMessage.error('五通道处理人配置读取失败');
|
||||
} finally {
|
||||
handlerLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const openHandlerSelector = (item: DirectHandlerConfig) => {
|
||||
if (!item.roleAvailable) return;
|
||||
selectedHandlerChannel.value = item;
|
||||
candidateKeyword.value = '';
|
||||
selectedCandidateUserIds.value = [];
|
||||
handlerCandidates.value = [];
|
||||
handlerDialogVisible.value = true;
|
||||
void loadHandlerCandidates();
|
||||
};
|
||||
|
||||
const loadHandlerCandidates = async () => {
|
||||
if (!selectedHandlerChannel.value || candidateLoading.value) return;
|
||||
candidateLoading.value = true;
|
||||
try {
|
||||
const result = await listDirectHandlerCandidates(
|
||||
selectedHandlerChannel.value.channelCode,
|
||||
candidateKeyword.value.trim()
|
||||
);
|
||||
handlerCandidates.value = result.data || [];
|
||||
selectedCandidateUserIds.value = selectedCandidateUserIds.value.filter((userId) =>
|
||||
handlerCandidates.value.some((candidate) => candidate.userId === userId)
|
||||
);
|
||||
} catch {
|
||||
handlerCandidates.value = [];
|
||||
selectedCandidateUserIds.value = [];
|
||||
ElMessage.error('候选处理人读取失败');
|
||||
} finally {
|
||||
candidateLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const assignSelectedHandlers = async () => {
|
||||
if (!selectedHandlerChannel.value || !selectedCandidateUserIds.value.length || handlerMutating.value) return;
|
||||
handlerMutating.value = true;
|
||||
try {
|
||||
const result = await assignDirectHandlers(selectedHandlerChannel.value.channelCode, {
|
||||
userIds: selectedCandidateUserIds.value,
|
||||
expectedTenantId: handlerTenantId.value
|
||||
});
|
||||
applyHandlerConfig(result.data);
|
||||
handlerDialogVisible.value = false;
|
||||
ElMessage.success('处理人已绑定;新角色将在该用户下次登录后生效');
|
||||
} finally {
|
||||
handlerMutating.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const removeHandler = async (item: DirectHandlerConfig, user: DirectHandlerUser) => {
|
||||
if (!item.roleAvailable || handlerMutating.value) return;
|
||||
const remainingHandlers = Math.max(0, item.users.length - 1);
|
||||
const readinessWarning = directHandlerReady(remainingHandlers, handlerMinimumHandlers.value)
|
||||
? ''
|
||||
: `\n解除后该通道仅剩 ${remainingHandlers} 名有效处理人,不满足主处理和替补要求。`;
|
||||
await ElMessageBox.confirm(
|
||||
`确认解除“${user.nickName || user.userName}”的${item.channelName}处理权限?${readinessWarning}`,
|
||||
'解除处理人',
|
||||
{
|
||||
type: 'warning',
|
||||
confirmButtonText: '确认解除',
|
||||
cancelButtonText: '取消'
|
||||
}
|
||||
);
|
||||
handlerMutating.value = true;
|
||||
try {
|
||||
const result = await removeDirectHandler(item.channelCode, {
|
||||
userId: user.userId,
|
||||
expectedTenantId: handlerTenantId.value
|
||||
});
|
||||
applyHandlerConfig(result.data);
|
||||
ElMessage.success('处理人权限已解除;已登录会话可能需要重新登录后更新');
|
||||
} finally {
|
||||
handlerMutating.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const load = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
@@ -188,9 +421,12 @@ const submitReply = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(load);
|
||||
onMounted(() => {
|
||||
void load();
|
||||
void loadHandlerConfig();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.direct-admin-page{padding:24px;color:#172033}.page-head,.filter-panel,.detail-meta{display:flex;align-items:center;gap:12px}.page-head{justify-content:space-between;margin-bottom:16px}.page-head h1{margin:0;font-size:28px}.page-head p{margin:8px 0 0;color:#667085}.panel{border:1px solid #e4e7ec;border-radius:12px;background:#fff;box-shadow:0 4px 18px rgba(16,24,40,.05)}.filter-panel{margin-top:14px;padding:14px}.filter-panel .el-select{width:180px}.table-panel{margin-top:14px;padding:8px}.pagination{justify-content:flex-end;padding:14px}.detail-meta{flex-wrap:wrap}.detail-time{color:#667085;font-size:13px}.feedback-copy,.reply-copy{white-space:pre-wrap;overflow-wrap:anywhere;padding:16px;border-radius:10px;background:#f7f8fa;line-height:1.8}.reply-copy{border-left:3px solid #c91825;background:#fff7f7}.reply-form{margin-top:24px}@media(max-width:900px){.direct-admin-page{padding:16px}.page-head{align-items:flex-start}.filter-panel{flex-wrap:wrap}}
|
||||
.direct-admin-page{padding:24px;color:#172033}.page-head,.filter-panel,.detail-meta,.handler-head,.handler-card-head,.handler-user,.candidate-toolbar{display:flex;align-items:center;gap:12px}.page-head,.handler-head{justify-content:space-between;margin-bottom:16px}.page-head h1,.handler-head h2{margin:0}.page-head h1{font-size:28px}.handler-head h2{font-size:20px}.page-head p,.handler-head p{margin:8px 0 0;color:#667085}.panel{border:1px solid #e4e7ec;border-radius:12px;background:#fff;box-shadow:0 4px 18px rgba(16,24,40,.05)}.handler-panel{margin-top:14px;padding:18px}.handler-grid{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:12px;min-height:120px;margin-top:14px}.handler-card{display:flex;min-width:0;flex-direction:column;gap:12px;padding:14px;border:1px solid #e4e7ec;border-radius:10px;background:#f8fafc}.handler-card-head{align-items:flex-start;justify-content:space-between}.handler-card-head strong,.handler-card-head small{display:block}.handler-card-head small,.handler-user small,.handler-role-missing,.candidate-list small{color:#667085;font-size:11px}.handler-users{display:grid;gap:7px}.handler-user{min-width:0;padding:7px 8px;border-radius:8px;background:#fff}.handler-user span{min-width:0;overflow:hidden;flex:1;text-overflow:ellipsis;white-space:nowrap}.handler-user small{display:none}.handler-empty{min-height:38px;color:#b42318;font-size:12px;line-height:1.55}.candidate-toolbar{margin-bottom:12px}.candidate-list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin-top:14px}.candidate-list :deep(.el-checkbox){box-sizing:border-box;width:100%;height:auto;min-height:44px;margin:0}.candidate-list small{display:block;margin-top:2px}.filter-panel{margin-top:14px;padding:14px}.filter-panel .el-select{width:180px}.table-panel{margin-top:14px;padding:8px}.pagination{justify-content:flex-end;padding:14px}.detail-meta{flex-wrap:wrap}.detail-time{color:#667085;font-size:13px}.feedback-copy,.reply-copy{white-space:pre-wrap;overflow-wrap:anywhere;padding:16px;border-radius:10px;background:#f7f8fa;line-height:1.8}.reply-copy{border-left:3px solid #c91825;background:#fff7f7}.reply-form{margin-top:24px}@media(max-width:1400px){.handler-grid{grid-template-columns:repeat(3,minmax(0,1fr))}}@media(max-width:900px){.direct-admin-page{padding:16px}.page-head,.handler-head{align-items:flex-start}.filter-panel{flex-wrap:wrap}.handler-grid,.candidate-list{grid-template-columns:1fr}}
|
||||
</style>
|
||||
|
||||
@@ -3,7 +3,8 @@ import { existsSync, readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
const frontendRoot = existsSync(resolve(process.cwd(), 'src')) ? process.cwd() : resolve(process.cwd(), 'frontend');
|
||||
const src = (...parts: string[]) => readFileSync(resolve(frontendRoot, 'src', ...parts), 'utf8');
|
||||
const src = (...parts: string[]) =>
|
||||
readFileSync(resolve(frontendRoot, 'src', ...parts), 'utf8').replace(/\r\n/g, '\n');
|
||||
|
||||
describe('knowledge space administration contract', () => {
|
||||
it('registers the controlled route and sidebar entry', () => {
|
||||
|
||||
Reference in New Issue
Block a user