按用户要求,把 NoticeFront 里这两块 UI 原样搬到助手表单(不再自制近似控件):
- 正文 → wangEditor v5(@wangeditor/editor + @wangeditor/editor-for-vue)。
提交给后端的是 **HTML 字符串**,与模块页 NoticeRequest.content 一致;
插图上传走 /api/file/UploadImage、预览走 /api/file/Preview?id=(地址由后端下发,
统一由编排层拼模块前缀)。异步 chunk 加载,避免把 800KB 编辑器塞进首屏。
- 通知对象 → 1:1 搬运模块页 components/UserCheckInput.vue + UserCheckModal.vue:
radio「全部用户 / 指定用户」+ 只读回显 + +/清空 + 选人弹窗(左 zTree 组织树勾选级联 /
右已选列表 / 全选 / 删除选中 / 按名字搜索回勾);v-model 值是字符串("all" 或 id 逗号串)。
- 全局依赖 jQuery + zTree 按模块页的方式由 index.html 全局加载(public/libs/),
不用 npm 引入 —— 保证加载顺序(zTree 必须在 jQuery 之后)与样式/图标与模块页完全一致。
已确认模块页的 lodash 实际未被使用,故不引入。
FormCard 新增两个 widget 分支,每一处都对齐模块页口径:
- richtext:初值取字符串、提交原 HTML,只读摘要用剥标签后的纯文本;
- userpicker:初值数组 join / 字符串原样(空则 allToken)、提交字符串;
★ 跳过下拉字典的「拍平 + 取值对齐」—— 那条路会把人员 id 串判成「认不出的值」清掉,
等于把模型/用户已经选好的人悄悄抹掉;
- 两者都独占整行(自适应多列会把单选组挤到换行)。
验证:.scratch/verify-notice-publish-widgets.mjs 真机 38/38(含正文真实打字、
两个数据源页签、勾选回显、以及提交体 receiveList 必须是字符串 "101"、content 必须是 <p>…)。
npm run build 通过(RichTextEditor 单独成 chunk)。
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
<template>
|
||||
<!-- 通知对象选择:全部用户 / 指定用户(自业务模块 NoticeFront 的 components/UserCheckInput.vue 照搬) -->
|
||||
<div class="check-user-wrap">
|
||||
<div class="check-user-box">
|
||||
<RadioGroup v-model="isCheckUserAll" class="radio" @on-change="userAllCheck">
|
||||
<Radio :disabled="disabled || editDisabled" :label="0">{{ allText }}</Radio>
|
||||
<Radio :disabled="disabled" :label="1">指定用户</Radio>
|
||||
</RadioGroup>
|
||||
<Input
|
||||
type="text"
|
||||
v-model="userNameStr"
|
||||
readonly
|
||||
:disabled="disabled || !isCheckUserAll"
|
||||
placeholder="选择指定的用户"
|
||||
class="input"
|
||||
></Input>
|
||||
<Button icon="md-add" :disabled="disabled || !isCheckUserAll" @click="checkUserShow"></Button>
|
||||
<Button icon="md-trash" :disabled="disabled || !isCheckUserAll" @click="resetUser"></Button>
|
||||
</div>
|
||||
<UserCheckModal ref="userCheckModal" :sources="tabs" :userids="tempUserIds" @on-submit="submitUser" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
/**
|
||||
* 「通知对象」输入控件 —— 自业务模块 NoticeFront 的 `components/UserCheckInput.vue` **照搬**。
|
||||
*
|
||||
* <p>v-model 值是**字符串**:`"all"` 或用户 id 逗号串(与模块页 `form.rdoname` 一致);
|
||||
* 切到「指定用户」会直接弹出选人弹窗(左组织树 / 右已选列表)。
|
||||
* 提交时由后端按 `joinSeparator` 拼串,这里原样给出字符串即可。
|
||||
*
|
||||
* <p>与本体的**唯一区别是数据从哪儿来**:模块里是 alova 直连自己的 `/api/*`,
|
||||
* 这里改为后端下发的 `sources[].call`(`FormField.pickerSources`),
|
||||
* 由 `callModuleApi` 带**用户登录态**去取 —— 模块接口只认模块容器的 CAS 会话,服务端代调必 302。
|
||||
* 取回的是**原始嵌套树**,不做拍平(拍平会丢层级,树就画不出来)。
|
||||
*/
|
||||
import UserCheckModal from "@/components/UserCheckModal.vue";
|
||||
import { callModuleApi } from "@/utils/module-session";
|
||||
import type { BrowserToolCall } from "@/types/agent";
|
||||
|
||||
/** 后端下发的数据源页签(每个 call 都已带模块前缀) */
|
||||
interface PickerSource {
|
||||
name: string;
|
||||
call: BrowserToolCall;
|
||||
}
|
||||
|
||||
/**
|
||||
* 把模块返回的原始响应解成**组织树数组**。
|
||||
*
|
||||
* <p>与 `parseOptionItems` 分开写:那个是给下拉用的,会把树**拍平成叶子**
|
||||
* (下拉里当然不该出现「年级」这一层)。选人弹窗要的恰恰是层级本身,
|
||||
* 所以这里只做「剥掉 {code,msg,data} 外壳」,不动结构。
|
||||
*/
|
||||
function parseTree(body?: string): any[] {
|
||||
if (!body) return [];
|
||||
let payload: any;
|
||||
try {
|
||||
payload = JSON.parse(body);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
if (payload && typeof payload === "object" && !Array.isArray(payload) && "code" in payload) {
|
||||
if (Number(payload.code) !== 0) return [];
|
||||
payload = payload.data;
|
||||
}
|
||||
if (Array.isArray(payload)) return payload;
|
||||
// 少数版本把数组包在 data.list / data.records 里,容错取第一个数组
|
||||
if (payload && typeof payload === "object") {
|
||||
const arr = Object.values(payload).find(v => Array.isArray(v));
|
||||
if (arr) return arr as any[];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export default {
|
||||
name: "UserCheckInput",
|
||||
components: { UserCheckModal },
|
||||
props: {
|
||||
/** 当前值:`"all"` 或用户 id 逗号串 */
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: "all",
|
||||
},
|
||||
/** 选人弹窗的数据源页签(后端下发的 pickerSources,已带模块前缀) */
|
||||
sources: {
|
||||
type: Array as () => PickerSource[],
|
||||
default: () => [],
|
||||
},
|
||||
/** 兜底数据源(后端只下发了 optionCall 时用;页签名固定「选择用户」) */
|
||||
fallbackCall: {
|
||||
type: Object as () => BrowserToolCall | undefined,
|
||||
default: undefined,
|
||||
},
|
||||
/** 「全部」哨兵值(后端下发,默认 all) */
|
||||
allToken: {
|
||||
type: String,
|
||||
default: "all",
|
||||
},
|
||||
/** 「全部」选项的显示名(后端下发,默认「全部用户」) */
|
||||
allLabel: {
|
||||
type: String,
|
||||
default: "全部用户",
|
||||
},
|
||||
/** 编辑态:把「全部用户」显示成「现有用户」(模块页同款) */
|
||||
isEdit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue", "names"],
|
||||
data() {
|
||||
return {
|
||||
isCheckUserAll: 0 as number,
|
||||
tempUserIds: [] as any[],
|
||||
userNameStr: "",
|
||||
editDisabled: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
/** 弹窗的页签:{ name, loader }(loader 返回原始组织树) */
|
||||
tabs(): { name: string; loader: () => Promise<any[]> }[] {
|
||||
const srcs: PickerSource[] = this.sources || [];
|
||||
if (srcs.length) {
|
||||
return srcs.map(s => ({
|
||||
name: s.name,
|
||||
loader: async () => parseTree((await callModuleApi(s.call)).body),
|
||||
}));
|
||||
}
|
||||
const fb = this.fallbackCall;
|
||||
if (fb) {
|
||||
return [{ name: "选择用户", loader: async () => parseTree((await callModuleApi(fb)).body) }];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
allText(): string {
|
||||
return this.isEdit ? "现有用户" : this.allLabel;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.syncFromValue();
|
||||
},
|
||||
methods: {
|
||||
/** 按当前 v-model 值同步界面(初值与外部改动共用一份口径) */
|
||||
syncFromValue() {
|
||||
const v = this.modelValue;
|
||||
if (v === this.allToken || !v) {
|
||||
this.isCheckUserAll = 0;
|
||||
this.userNameStr = v === this.allToken ? this.allText : "";
|
||||
} else {
|
||||
this.isCheckUserAll = 1;
|
||||
this.tempUserIds = parseIntList(v);
|
||||
}
|
||||
},
|
||||
checkUserShow() {
|
||||
(this.$refs.userCheckModal as any)?.show();
|
||||
},
|
||||
resetUser() {
|
||||
// 清空指定用户 → 回到「全部用户」
|
||||
this.tempUserIds = [];
|
||||
this.userNameStr = "";
|
||||
this.$emit("update:modelValue", this.allToken);
|
||||
this.$emit("names", this.allText);
|
||||
},
|
||||
userAllCheck(type: number) {
|
||||
const tempIds = parseIntList(this.modelValue);
|
||||
if (type == 0) {
|
||||
if (this.isEdit) {
|
||||
this.tempUserIds = tempIds;
|
||||
this.userNameStr = "现有用户";
|
||||
this.$emit("update:modelValue", String(tempIds));
|
||||
this.$emit("names", "现有用户");
|
||||
} else {
|
||||
this.tempUserIds = [];
|
||||
this.userNameStr = this.allLabel;
|
||||
this.$emit("update:modelValue", this.allToken);
|
||||
this.$emit("names", this.allLabel);
|
||||
}
|
||||
} else {
|
||||
if (this.isEdit) {
|
||||
this.editDisabled = true;
|
||||
}
|
||||
this.tempUserIds = tempIds;
|
||||
this.userNameStr = "";
|
||||
this.checkUserShow();
|
||||
}
|
||||
},
|
||||
submitUser(arr: any[]) {
|
||||
// 确认用户
|
||||
this.userNameStr = arr.map((x: any) => x.name).join("、");
|
||||
const ids = arr.map((x: any) => x.id).join(",");
|
||||
this.$emit("update:modelValue", ids || this.allToken);
|
||||
this.$emit("names", this.userNameStr || this.allText);
|
||||
if (!arr.length) {
|
||||
// 一个人都没选 → 回到「全部用户」(模块页同款)
|
||||
this.isCheckUserAll = 0;
|
||||
this.userNameStr = this.allLabel;
|
||||
this.$emit("update:modelValue", this.allToken);
|
||||
this.$emit("names", this.allLabel);
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
modelValue() {
|
||||
this.syncFromValue();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/** `"101,102"` → `[101,102]`(非数字段丢掉,与模块页 `split().map(parseInt).filter` 同口径) */
|
||||
function parseIntList(v: any): number[] {
|
||||
return String(v || "")
|
||||
.split(",")
|
||||
.map(x => parseInt(x))
|
||||
.filter(x => !isNaN(x));
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.check-user-wrap {
|
||||
width: 100%;
|
||||
}
|
||||
.check-user-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.check-user-box .radio {
|
||||
width: 180px;
|
||||
flex: none;
|
||||
font-size: 12px;
|
||||
vertical-align: middle;
|
||||
line-height: 32px;
|
||||
}
|
||||
.check-user-box .input {
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user