build / build (push) Successful in 2m43s
承接上一提交(通知公告的两个控件),把其余模块里同类控件一次搬到位。
新增组件(都照搬对应模块页的实现,只把「数据从哪来」换成后端下发的 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
215 lines
7.7 KiB
Vue
215 lines
7.7 KiB
Vue
<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>
|