fix: 取消消息模板默认来源
This commit is contained in:
parent
9d7f3c272c
commit
b96003edcc
@ -56,7 +56,7 @@ pnpm run build:antdv-next
|
|||||||
- 系统管理 / 网络管理使用 TSX、KtTable 与统一 Vben 表单维护 API 持久化的 TCP/UDP 单端口转发期望状态,并通过独立页签管理腾讯云云解析 DNS 的 A/AAAA 自动更新绑定。页面展示 Agent、路由同步、UDP Keeper、公网端点租约和 DDNS 同步状态,支持异步 CRUD、重试、Keeper 启停、立即 STUN 刷新及端点历史;A 只使用合格 UDP Keeper 的公网 IPv4,AAAA 只使用 Agent 全局 IPv6,DNS 值不包含端口。首屏读取一次 HTTP 快照,后续只按 API SSE 的资源来源刷新当前活动页签;心跳和其他页签事件不刷新,不使用定时轮询。`Page autoContentHeight` 中的 Tabs 与 KtTable 必须由满高纵向 flex 外壳承接,活动面板保持 `flex-1 min-h-0`,否则 KtTable 的百分比高度链会塌陷。Admin 不接触路由器、MQTT 或腾讯云凭据;当前已验证切片只执行 UDP 路由器写入,TCP 可保存 CRUD 期望但 Agent 会显示设备协议门禁失败,STUN 操作保持可见且禁用。
|
- 系统管理 / 网络管理使用 TSX、KtTable 与统一 Vben 表单维护 API 持久化的 TCP/UDP 单端口转发期望状态,并通过独立页签管理腾讯云云解析 DNS 的 A/AAAA 自动更新绑定。页面展示 Agent、路由同步、UDP Keeper、公网端点租约和 DDNS 同步状态,支持异步 CRUD、重试、Keeper 启停、立即 STUN 刷新及端点历史;A 只使用合格 UDP Keeper 的公网 IPv4,AAAA 只使用 Agent 全局 IPv6,DNS 值不包含端口。首屏读取一次 HTTP 快照,后续只按 API SSE 的资源来源刷新当前活动页签;心跳和其他页签事件不刷新,不使用定时轮询。`Page autoContentHeight` 中的 Tabs 与 KtTable 必须由满高纵向 flex 外壳承接,活动面板保持 `flex-1 min-h-0`,否则 KtTable 的百分比高度链会塌陷。Admin 不接触路由器、MQTT 或腾讯云凭据;当前已验证切片只执行 UDP 路由器写入,TCP 可保存 CRUD 期望但 Agent 会显示设备协议门禁失败,STUN 操作保持可见且禁用。
|
||||||
- Vue i18n 文案中的普通 `@` 必须写成字面量插值 `{'@'}`,否则生产消息编译器会把它识别为 linked message 语法。网络管理语言包由 `network-locale.spec.ts` 逐条通过实际 i18n runtime 校验,不能只依赖 JSON 解析或组件测试里的 `$t` mock。
|
- Vue i18n 文案中的普通 `@` 必须写成字面量插值 `{'@'}`,否则生产消息编译器会把它识别为 linked message 语法。网络管理语言包由 `network-locale.spec.ts` 逐条通过实际 i18n runtime 校验,不能只依赖 JSON 解析或组件测试里的 `$t` mock。
|
||||||
- QQBot / 账号连接页拆分 OneBot 连接、QQ 登录、NapCat 运行和运行说明列;更新登录通过 SSE 展示 quick / password / captcha / new-device / qrcode 每步中文进度,密码登录触发 QQ 安全验证时在弹窗内完成腾讯验证码并回交 API,新设备验证二维码和腾讯验证码分开展示;行操作“运行态”打开只读抽屉,展示 NapCat runtime/protocol/session behavior profile、风险模式和登录事件证据。
|
- QQBot / 账号连接页拆分 OneBot 连接、QQ 登录、NapCat 运行和运行说明列;更新登录通过 SSE 展示 quick / password / captcha / new-device / qrcode 每步中文进度,密码登录触发 QQ 安全验证时在弹窗内完成腾讯验证码并回交 API,新设备验证二维码和腾讯验证码分开展示;行操作“运行态”打开只读抽屉,展示 NapCat runtime/protocol/session behavior profile、风险模式和登录事件证据。
|
||||||
- QQBot / 消息订阅与消息模板是两个平级菜单;新建订阅不默认选择消息源,选择后才按该来源的 `subscriptionFields` 动态生成字段并加载候选项,不把通用订阅表单绑定到 STUN。账号配置第四页签用于为当前 QQBot 选择订阅、模板以及群聊/私聊目标,不提供跨账号选择,两个目标选择框固定填满横向表单宽度。模板编辑器输入 `$` 后通过 Mentions 候选精确插入 `${{变量}}`。三个入口只在首次进入、显式刷新或成功写操作后更新列表,不使用后台轮询;菜单、按钮与账号页签分别受 `QqBot:MessageSubscription:*`、`QqBot:MessageTemplate:*` 和 `QqBot:Account:MessagePush:*` 权限控制。
|
- QQBot / 消息订阅与消息模板是两个平级菜单;新建订阅和新建模板均不默认选择消息源。订阅选择来源后才按该来源的 `subscriptionFields` 动态生成字段并加载候选项,不把通用订阅表单绑定到 STUN;模板选择来源后才加载变量详情,输入 `$` 后通过 Mentions 候选精确插入 `${{变量}}`。账号配置第四页签用于为当前 QQBot 选择订阅、模板以及群聊/私聊目标,不提供跨账号选择,两个目标选择框固定填满横向表单宽度。三个入口只在首次进入、显式刷新或成功写操作后更新列表,不使用后台轮询;菜单、按钮与账号页签分别受 `QqBot:MessageSubscription:*`、`QqBot:MessageTemplate:*` 和 `QqBot:Account:MessagePush:*` 权限控制。
|
||||||
|
|
||||||
源码目录禁止同级存放单元测试或 `__tests__`;全部单元测试统一放在根目录单数 `test/`,应用测试直接使用 `test/api`、`test/components`、`test/router`、`test/store`、`test/views`,共享包与内部工具分别使用 `test/packages`、`test/internal`,结构门禁位于 `test/governance`。
|
源码目录禁止同级存放单元测试或 `__tests__`;全部单元测试统一放在根目录单数 `test/`,应用测试直接使用 `test/api`、`test/components`、`test/router`、`test/store`、`test/views`,共享包与内部工具分别使用 `test/packages`、`test/internal`,结构门禁位于 `test/governance`。
|
||||||
|
|
||||||
|
|||||||
@ -124,7 +124,7 @@ export default defineComponent({
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
name: '',
|
name: '',
|
||||||
remark: '',
|
remark: '',
|
||||||
sourceKey: props.sources[0]?.sourceKey || '',
|
sourceKey: '',
|
||||||
},
|
},
|
||||||
} satisfies MessageTemplateModalData)
|
} satisfies MessageTemplateModalData)
|
||||||
.open();
|
.open();
|
||||||
|
|||||||
@ -233,7 +233,7 @@ describe('message template modal', () => {
|
|||||||
).toBe(500);
|
).toBe(500);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('opens before reset/set/reset-validation and isolates edit then create', async () => {
|
it('opens create with an empty source and does not preload source detail', async () => {
|
||||||
const wrapper = mountModal();
|
const wrapper = mountModal();
|
||||||
mocks.formApi.resetForm.mockImplementation(async () => {
|
mocks.formApi.resetForm.mockImplementation(async () => {
|
||||||
expect(mocks.modalApi.open).toHaveBeenCalled();
|
expect(mocks.modalApi.open).toHaveBeenCalled();
|
||||||
@ -249,8 +249,15 @@ describe('message template modal', () => {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
name: '',
|
name: '',
|
||||||
remark: '',
|
remark: '',
|
||||||
sourceKey: 'source-a',
|
sourceKey: '',
|
||||||
});
|
});
|
||||||
|
expect(mocks.detail).toHaveBeenCalledOnce();
|
||||||
|
expect(mocks.detail).toHaveBeenLastCalledWith('source-b');
|
||||||
|
const content = mocks.formOptions.schema.find(
|
||||||
|
(field: any) => field.fieldName === 'content',
|
||||||
|
);
|
||||||
|
expect(content.componentProps().disabled).toBe(true);
|
||||||
|
expect(content.componentProps().variables).toEqual([]);
|
||||||
const resetOrder =
|
const resetOrder =
|
||||||
mocks.formApi.resetForm.mock.invocationCallOrder.at(-1) || 0;
|
mocks.formApi.resetForm.mock.invocationCallOrder.at(-1) || 0;
|
||||||
const setOrder =
|
const setOrder =
|
||||||
@ -418,6 +425,10 @@ describe('message template modal', () => {
|
|||||||
const wrapper = mountModal();
|
const wrapper = mountModal();
|
||||||
(wrapper.vm as any).openCreate();
|
(wrapper.vm as any).openCreate();
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
const initialLoadPromise = mocks.formOptions.handleValuesChange(
|
||||||
|
{ ...mocks.formValues, sourceKey: 'source-a' },
|
||||||
|
['sourceKey'],
|
||||||
|
);
|
||||||
expect(mocks.detail).toHaveBeenNthCalledWith(1, 'source-a');
|
expect(mocks.detail).toHaveBeenNthCalledWith(1, 'source-a');
|
||||||
const switchPromise = mocks.formOptions.handleValuesChange(
|
const switchPromise = mocks.formOptions.handleValuesChange(
|
||||||
{ ...mocks.formValues, sourceKey: 'source-b' },
|
{ ...mocks.formValues, sourceKey: 'source-b' },
|
||||||
@ -433,7 +444,7 @@ describe('message template modal', () => {
|
|||||||
await switchPromise;
|
await switchPromise;
|
||||||
expect(content.componentProps().variables[0].key).toBe('port');
|
expect(content.componentProps().variables[0].key).toBe('port');
|
||||||
deferred.get('source-a')?.resolve(createSource('source-a', 'endpoint'));
|
deferred.get('source-a')?.resolve(createSource('source-a', 'endpoint'));
|
||||||
await flushPromises();
|
await initialLoadPromise;
|
||||||
expect(content.componentProps().variables[0].key).toBe('port');
|
expect(content.componentProps().variables[0].key).toBe('port');
|
||||||
expect(mocks.formApi.validateField).toHaveBeenCalledWith('content');
|
expect(mocks.formApi.validateField).toHaveBeenCalledWith('content');
|
||||||
});
|
});
|
||||||
@ -453,14 +464,17 @@ describe('message template modal', () => {
|
|||||||
const wrapper = mountModal();
|
const wrapper = mountModal();
|
||||||
(wrapper.vm as any).openCreate();
|
(wrapper.vm as any).openCreate();
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
const initialLoad = mocks.formOptions.handleValuesChange(
|
||||||
|
{ ...mocks.formValues, sourceKey: 'source-a' },
|
||||||
|
['sourceKey'],
|
||||||
|
);
|
||||||
const collidingLoad = mocks.formOptions.handleValuesChange(
|
const collidingLoad = mocks.formOptions.handleValuesChange(
|
||||||
{ ...mocks.formValues, sourceKey: 'source-a' },
|
{ ...mocks.formValues, sourceKey: 'source-a' },
|
||||||
['sourceKey'],
|
['sourceKey'],
|
||||||
);
|
);
|
||||||
expect(mocks.detail).toHaveBeenCalledTimes(1);
|
expect(mocks.detail).toHaveBeenCalledTimes(1);
|
||||||
resolveSourceA(createSource('source-a', 'endpoint'));
|
resolveSourceA(createSource('source-a', 'endpoint'));
|
||||||
await collidingLoad;
|
await Promise.all([initialLoad, collidingLoad]);
|
||||||
await flushPromises();
|
|
||||||
|
|
||||||
mocks.detail.mockRejectedValueOnce(new Error('detail failed'));
|
mocks.detail.mockRejectedValueOnce(new Error('detail failed'));
|
||||||
const failedSourceChange = mocks.formOptions.handleValuesChange(
|
const failedSourceChange = mocks.formOptions.handleValuesChange(
|
||||||
@ -485,12 +499,16 @@ describe('message template modal', () => {
|
|||||||
expect(content.componentProps().loading).toBe(false);
|
expect(content.componentProps().loading).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('contains a non-awaited modal-open detail failure and remains usable for retry', async () => {
|
it('contains a source-change detail failure and remains usable for retry', async () => {
|
||||||
mocks.detail.mockRejectedValueOnce(new Error('open detail failed'));
|
mocks.detail.mockRejectedValueOnce(new Error('open detail failed'));
|
||||||
const wrapper = mountModal();
|
const wrapper = mountModal();
|
||||||
|
|
||||||
(wrapper.vm as any).openCreate();
|
(wrapper.vm as any).openCreate();
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
await mocks.formOptions.handleValuesChange(
|
||||||
|
{ ...mocks.formValues, sourceKey: 'source-a' },
|
||||||
|
['sourceKey'],
|
||||||
|
);
|
||||||
|
|
||||||
const content = mocks.formOptions.schema.find(
|
const content = mocks.formOptions.schema.find(
|
||||||
(field: any) => field.fieldName === 'content',
|
(field: any) => field.fieldName === 'content',
|
||||||
@ -500,8 +518,10 @@ describe('message template modal', () => {
|
|||||||
expect(wrapper.find('footer button').exists()).toBe(true);
|
expect(wrapper.find('footer button').exists()).toBe(true);
|
||||||
|
|
||||||
mocks.detail.mockResolvedValueOnce(createSource('source-a', 'endpoint'));
|
mocks.detail.mockResolvedValueOnce(createSource('source-a', 'endpoint'));
|
||||||
(wrapper.vm as any).openCreate();
|
await mocks.formOptions.handleValuesChange(
|
||||||
await flushPromises();
|
{ ...mocks.formValues, sourceKey: 'source-a' },
|
||||||
|
['sourceKey'],
|
||||||
|
);
|
||||||
|
|
||||||
expect(mocks.detail).toHaveBeenCalledTimes(2);
|
expect(mocks.detail).toHaveBeenCalledTimes(2);
|
||||||
expect(content.componentProps().variables[0].key).toBe('endpoint');
|
expect(content.componentProps().variables[0].key).toBe('endpoint');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user