Files
yunhai-agent-front/src/components/UserCheckInput.vue
T
lijing b0f31608d4
build / build (push) Successful in 2m43s
feat(表单): 正文接入模块页同款富文本,公告/宿舍的接收对象接入自绘控件
承接上一提交(通知公告的两个控件),把其余模块里同类控件一次搬到位。

新增组件(都照搬对应模块页的实现,只把「数据从哪来」换成后端下发的 pickerSources)
  · ScopePickerInput.vue ← 校园公告 components/ClassScopeInput.vue
    (全部班级 / 指定班级 + 只读回显 + 弹窗班级树;v-model 是 scope 对象数组)
  · RoomPickerInput.vue ← 宿舍通知 views/dorm/NewNotice.vue 的「选择接收宿舍」弹窗
    (左楼栋列表 / 右按楼层分组的房间勾选 / 底部已选间数与住宿生人数;v-model 是字符串)
  · utils/picker-tree.ts:只剥 {code,msg,data} 外壳、**不拍平**
    (弹窗要的是原始嵌套树;拍平是下拉字典的口径)

改造
  · RichTextEditor:后端没下发正文图片上传地址时,把工具栏的「图片」整组摘掉
    —— 专业发展的编辑器就是这样(该模块没有 UploadImage)
  · UserCheckInput:抽出公共 parseTree,三个自绘控件共用一份外壳剥离口径
  · FormCard:接入 scopepicker / roompicker(初值、序列化、isWide 独占整行)
  · types/agent.ts:widget 联合类型补 scopepicker / roompicker
2026-10-14 16:56:50 +08:00

215 lines
7.7 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.
<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 { parseTree } from "@/utils/picker-tree";
import type { BrowserToolCall } from "@/types/agent";
/** 后端下发的数据源页签(每个 call 都已带模块前缀) */
interface PickerSource {
name: string;
call: BrowserToolCall;
}
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>