| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653 |
- <template>
- <view class="page-no-tab">
- <uni-nav-bar
- fixed="true"
- statusBar="true"
- left-icon="back"
- title="转正申请模板"
- background-color="#157A2C"
- color="#fff"
- @clickLeft="back"
- ></uni-nav-bar>
- <view class="form-intro">
- <ModuleIcon :icon="meta.icon" :color="meta.color" soft />
- <view>
- <text class="intro-title">{{ form.id ? '编辑模板' : '新增模板' }}</text>
- <text class="intro-sub">{{ introSub }}</text>
- </view>
- </view>
- <view v-if="form.id && !isDraft" class="notice">
- <text>
- 已{{ form.templateStatusLabel || '启用' }}模板:正文与变量改完后需点底部「发布新版本」保存;
- 发布后模板会自动回到停用态,需重新启用。分类暂不支持修改。
- </text>
- </view>
- <view class="section-title"><text>基础信息</text></view>
- <view class="form-card card section">
- <view class="field">
- <text class="label required">模板名称</text>
- <input v-model="form.templateName" class="text-input" maxlength="128" placeholder="请输入模板名称" />
- </view>
- <view class="divider"></view>
- <view class="field">
- <text class="label required">模板编码</text>
- <input v-model="form.templateCode" class="text-input" maxlength="64" placeholder="同租户唯一" />
- </view>
- <view class="divider"></view>
- <hr-picker-row
- v-bind="pickerRowMetrics"
- label="模板分类"
- required
- :value="form.categoryId"
- :display="categoryLabel"
- :options="categoryOptions"
- :disabled="formDisabled"
- @change="onCategoryChange"
- />
- <view class="divider"></view>
- <hr-picker-row
- v-bind="pickerRowMetrics"
- label="所属机构"
- required
- :value="form.legalId"
- :display="form.legalName"
- :options="companies.map((item) => ({ label: item.name, value: item.id }))"
- :disabled="formDisabled || !companies.length"
- hint="机构列表为空"
- @change="onLegalChange"
- />
- <view class="divider"></view>
- <hr-picker-row
- v-bind="pickerRowMetrics"
- mode="tree"
- label="所属组织"
- required
- :value="form.deptId"
- :display="form.deptPath"
- :tree="deptTree"
- :disabled="formDisabled || !form.legalId"
- hint="请先选择所属机构"
- @change="onDeptPick"
- />
- <view class="divider"></view>
- <view class="field textarea-field">
- <text class="label">模板说明</text>
- <textarea v-model="form.remark" maxlength="2000" placeholder="选填,说明模板适用场景" />
- <text class="counter">{{ form.remark.length }}/2000</text>
- </view>
- </view>
- <view class="section-title"><text>模板正文</text></view>
- <view class="form-card card section">
- <view class="field textarea-field">
- <text class="label required">A4 画布正文</text>
- <textarea
- v-model="form.documentContent"
- placeholder="JSON 或 HTML 文本"
- />
- <text class="hint">{{ contentHint }}</text>
- </view>
- <view class="divider"></view>
- <view class="field textarea-field">
- <text class="label">变量定义</text>
- <textarea
- v-model="form.variablesJson"
- placeholder="变量定义 JSON 数组,选填"
- />
- </view>
- </view>
- <view class="section-title"><text>画布预览</text></view>
- <view class="form-card card section">
- <A4Preview :content="form.documentContent" />
- </view>
- <block v-if="form.id">
- <view class="section-title"><text>版本</text></view>
- <view class="form-card card section">
- <view class="field">
- <text class="label">当前版本</text>
- <text class="value">V{{ form.currentVersionNo || 1 }} · {{ form.versionStatus || '—' }}</text>
- </view>
- <view class="divider"></view>
- <view class="field textarea-field">
- <text class="label">新版本变更说明</text>
- <textarea v-model="versionChangeRemark" maxlength="200" placeholder="发布新版本时记录,选填" />
- </view>
- <view class="divider"></view>
- <view class="field press" @click="onPublishVersion">
- <text class="label">发布新版本</text>
- <text class="value link">用当前正文发布 ›</text>
- </view>
- </view>
- </block>
- <view class="footer-tip muted"><text>{{ footerTip }}</text></view>
- <view class="bottom-action">
- <view class="draft-btn press" @click="onCancel">取消</view>
- <view class="submit-btn press" @click="onSubmit">{{ submitLabel }}</view>
- </view>
- </view>
- </template>
- <script>
- import A4Preview from "@/pages/hr/components/A4Preview.vue";
- import ModuleIcon from "@/pages/hr/components/ModuleIcon.vue";
- import { applicationTypes } from "@/pages/hr/data/apply-config";
- import { listOrganizations } from "@/api/hr/organization";
- import { buildDeptTreeUnderLegal } from "@/utils/hr/organization-tree";
- import { getOrgOptionsWithFallback } from "@/utils/hr/current-org";
- import HrPickerRow from "@/components/hr/HrPickerRow.vue";
- import {
- TEMPLATE_STATUS_LABELS,
- loadOnlineTemplateCategories,
- loadOnlineTemplateDetail,
- publishOnlineTemplateVersion,
- saveOnlineTemplate,
- } from "@/api/hr/onlineDocument";
- function emptyForm() {
- return {
- id: "",
- templateName: "",
- templateCode: "",
- categoryId: "",
- legalId: "",
- legalName: "",
- deptId: "",
- deptPath: "",
- remark: "",
- documentContent: "",
- variablesJson: "",
- templateStatus: "",
- templateStatusLabel: "",
- versionStatus: "",
- currentVersionNo: 1,
- };
- }
- export default {
- components: { A4Preview, ModuleIcon, HrPickerRow },
- data() {
- return {
- form: emptyForm(),
- orgList: [],
- companies: [],
- deptTree: [],
- categories: [],
- versionChangeRemark: "",
- submitting: false,
- };
- },
- computed: {
- /** 统一选择行(HrPickerRow)的行度量:与本页 .field/.label 口径一致,避免左侧对不齐 */
- pickerRowMetrics() {
- return {
- rowMinHeight: "108rpx",
- rowPadding: "28rpx",
- labelWidth: "200rpx",
- };
- },
- meta() {
- return applicationTypes.regular || { title: "转正申请", subtitle: "", icon: "正", color: "#269a99" };
- },
- /** 只有草稿模板允许直接改正文 / 变量 / 分类,其余要走「发布新版本」 */
- isDraft() {
- return !this.form.id || this.form.templateStatus === "DRAFT";
- },
- introSub() {
- return this.form.id
- ? `${this.form.templateStatusLabel || TEMPLATE_STATUS_LABELS[this.form.templateStatus] || ""} · 当前版本 V${this.form.currentVersionNo || 1}`
- : "线上 Word 模板,供转正申请选用";
- },
- submitLabel() {
- if (!this.form.id) return "创建模板";
- return this.isDraft ? "保存修改" : "发布新版本";
- },
- contentHint() {
- return this.isDraft
- ? "变量用双花括号占位,例如 {{EMPLOYEE_NAME}}"
- : "已启用 / 停用模板:改完正文点底部「发布新版本」生效";
- },
- footerTip() {
- return "保存走 /hr/online-documents/templates,新建后默认为停用,需手动启用";
- },
- /** 模板分类候选(统一选择器用) */
- categoryOptions() {
- return (this.categories || []).map((item) => ({ label: item.categoryName, value: String(item.id) }));
- },
- categoryLabel() {
- if (!this.form.categoryId) return "";
- const hit = this.categories.find((item) => String(item.id) === String(this.form.categoryId));
- return hit ? hit.categoryName : `分类 ${this.form.categoryId}`;
- },
- },
- onLoad(options) {
- this.init(options || {});
- },
- methods: {
- validate() {
- if (!this.form.templateName.trim()) return "请填写模板名称";
- if (!this.form.templateCode.trim()) return "请填写模板编码";
- if (!this.form.categoryId) return "请选择模板分类";
- if (!this.form.deptId) return "请选择所属组织";
- if (!this.form.documentContent.trim()) return "请填写模板正文";
- const variables = this.form.variablesJson.trim();
- if (variables) {
- try {
- const parsed = JSON.parse(variables);
- if (!Array.isArray(parsed)) return "变量定义需为 JSON 数组";
- } catch (error) {
- return "变量定义不是合法 JSON";
- }
- }
- return "";
- },
- async refreshOrganizations() {
- try {
- // 组织树取全量:部门级联要按当前机构从中过滤出部门
- this.orgList = await listOrganizations();
- } catch (error) {
- this.orgList = [];
- uni.showToast({ title: "组织列表加载失败", icon: "none" });
- }
- // 机构候选与 PC 一致:只给当前账号所属公司(登录缓存 loginChangeGroupVOList),
- // 不再列全量机构树;缺名称时回落组织树,编辑老数据时补当前项
- this.companies = getOrgOptionsWithFallback(this.orgList, this.form.legalId, this.form.legalName);
- },
- refreshDeptTree(legalId) {
- // 所属机构已经在上面选过,不再作为部门级联的第一列重复展示:
- // 直接从部门层开始,选出来的部门路径也不带机构名。
- const nodes = legalId ? buildDeptTreeUnderLegal(this.orgList, legalId) : [];
- const legal = nodes.find((node) => String(node.id) === String(legalId)) || nodes[0];
- this.deptTree = legal && Array.isArray(legal.children) ? legal.children : [];
- },
- /** 在组织树里定位 deptId 所在的三级索引,用于回填 picker */
- findDeptIndex(tree, deptId) {
- const list = tree || [];
- const key = String(deptId || "");
- for (let i = 0; i < list.length; i += 1) {
- if (String(list[i].id) === key) return [i, 0, 0];
- const level2 = list[i].children || [];
- for (let j = 0; j < level2.length; j += 1) {
- if (String(level2[j].id) === key) return [i, j, 0];
- const level3 = level2[j].children || [];
- for (let k = 0; k < level3.length; k += 1) {
- if (String(level3[k].id) === key) return [i, j, k];
- }
- }
- }
- return [0, 0, 0];
- },
- /** 由模板的 deptId 反查所属机构与组织路径(编辑态回填,避免保存时丢组织) */
- restoreOrganization(deptId) {
- this.form.legalId = "";
- this.form.legalName = "";
- this.form.deptPath = "";
- this.form.deptId = deptId ? String(deptId) : "";
- if (!this.form.deptId) return;
- const byId = new Map(this.orgList.map((item) => [String(item.id), item]));
- const node = byId.get(this.form.deptId);
- // 组织列表里查不到(跨机构 / 已停用组织)时保留原主键,只做提示
- if (!node) {
- this.form.deptPath = `组织 ${this.form.deptId}`;
- return;
- }
- const isCompany = (item) => this.companies.some((company) => String(company.id) === String(item.id));
- // 自下而上收集祖先链(含自身)
- const chain = [];
- const visited = new Set();
- let cursor = node;
- while (cursor && !visited.has(String(cursor.id))) {
- visited.add(String(cursor.id));
- chain.unshift(cursor);
- const parentId = cursor.parentId == null ? "0" : String(cursor.parentId);
- cursor = parentId === "0" ? null : byId.get(parentId) || null;
- }
- const legal = chain.filter(isCompany)[0];
- const deptNodes = chain.filter((item) => !isCompany(item));
- const tail = deptNodes.length ? deptNodes : chain;
- this.form.deptId = tail[tail.length - 1].id;
- this.form.deptPath = tail.map((item) => item.name).filter(Boolean).join(" / ");
- if (!legal) return;
- this.form.legalId = legal.id;
- this.form.legalName = legal.name;
- this.refreshDeptTree(legal.id);
- this.deptIndex = this.findDeptIndex(this.deptTree, this.form.deptId);
- },
- chooseCategory() {
- if (!this.isDraft) {
- uni.showToast({ title: "已启用 / 停用模板不可改分类,请发布新版本", icon: "none" });
- return;
- }
- const options = this.categories;
- if (!options.length) {
- uni.showModal({
- title: "模板分类",
- content: "后端暂未提供分类列表接口,请直接填写分类主键(数字)",
- showCancel: false,
- success: () => this.promptCategoryId(),
- });
- return;
- }
- this.form.categoryId = options[0] ? String(options[0].id) : "";
- },
- /** 模板分类(统一选择器):选中后写入主键;需要手填主键时走 promptCategoryId */
- onCategoryChange(categoryId) {
- if (this.formDisabled) return;
- const hit = this.categories.find((item) => String(item.id) === String(categoryId));
- this.form.categoryId = hit ? String(hit.id) : String(categoryId);
- },
- promptCategoryId() {
- uni.showModal({
- title: "输入分类主键",
- editable: true,
- placeholderText: "请输入模板分类主键(数字)",
- success: (r) => {
- if (!r.confirm) return;
- const text = String(r.content || "").trim();
- if (!text) return;
- this.form.categoryId = text;
- },
- });
- },
- onLegalChange(legalId) {
- if (this.formDisabled) return;
- const picked = this.companies.find((item) => String(item.id) === String(legalId));
- if (!picked) return;
- this.form.legalId = picked.id;
- this.form.legalName = picked.name;
- this.form.deptId = "";
- this.form.deptPath = "";
- this.refreshDeptTree(picked.id);
- },
- /** 所属组织:树选择器回传节点 id,这里按树补出路径名 */
- onDeptPick(deptId) {
- const chain = this.deptChainById(this.deptTree, deptId);
- this.form.deptId = deptId;
- this.form.deptPath = chain.map((node) => node.name).filter(Boolean).join(" / ");
- },
- /** 在组织树里按 id 取「根 → 自身」的节点链 */
- deptChainById(tree, id, parents = []) {
- for (const node of tree || []) {
- const path = parents.concat(node);
- if (String(node.id) === String(id)) return path;
- const hit = this.deptChainById(node.children || [], id, path);
- if (hit.length) return hit;
- }
- return [];
- },
- async onSave() {
- if (this.submitting) return;
- const error = this.validate();
- if (error) {
- uni.showToast({ title: error, icon: "none" });
- return;
- }
- this.submitting = true;
- uni.showLoading({ title: "保存中..." });
- try {
- await saveOnlineTemplate({
- form: Object.assign({}, this.form),
- existingId: this.form.id || null,
- });
- uni.hideLoading();
- uni.showToast({ title: this.form.id ? "已保存" : "模板已创建", icon: "success" });
- setTimeout(() => this.onCancel(), 800);
- } catch (errorSave) {
- uni.hideLoading();
- uni.showToast({ title: (errorSave && errorSave.message) || "保存失败", icon: "none" });
- } finally {
- this.submitting = false;
- }
- },
- /** 底部主按钮:草稿直接保存,已启用 / 停用走发布新版本 */
- onSubmit() {
- if (this.submitting) return;
- if (this.isDraft) return this.onSave();
- return this.onPublishVersion();
- },
- async onPublishVersion() {
- if (!this.form.id) return;
- if (!this.form.documentContent.trim()) {
- uni.showToast({ title: "请先填写模板正文", icon: "none" });
- return;
- }
- uni.showModal({
- title: "发布新版本",
- content: "将用当前正文创建新版本,确认发布?",
- success: async (r) => {
- if (!r.confirm) return;
- uni.showLoading({ title: "发布中..." });
- try {
- await publishOnlineTemplateVersion(this.form.id, {
- documentContent: this.form.documentContent,
- variablesJson: this.form.variablesJson,
- changeRemark: this.versionChangeRemark,
- });
- uni.hideLoading();
- this.versionChangeRemark = "";
- // 发布新版本会自动停用模板并生成草稿版本,重新拉一次详情同步状态
- await this.loadTemplate(this.form.id);
- uni.showToast({ title: "新版本已发布", icon: "success" });
- } catch (error) {
- uni.hideLoading();
- uni.showToast({ title: (error && error.message) || "发布失败", icon: "none" });
- }
- },
- });
- },
- onCancel() {
- uni.redirectTo({ url: "/pages/hr/manage/regular-template-list" });
- },
- /** 拉取模板详情并回填表单(编辑进入与发布新版本后复用) */
- async loadTemplate(id) {
- const row = await loadOnlineTemplateDetail(id);
- if (!row) throw new Error("未找到该模板");
- Object.assign(this.form, {
- id: row.id,
- templateName: row.templateName || "",
- templateCode: row.templateCode || "",
- categoryId: row.categoryId || "",
- remark: row.remark || "",
- documentContent: row.documentContent || "",
- variablesJson: row.variablesJson || "",
- templateStatus: row.templateStatus || "",
- templateStatusLabel: row.templateStatusLabel || "",
- versionStatus: row.versionStatus || "",
- currentVersionNo: row.currentVersionNo || 1,
- });
- // 详情里没有机构 / 组织路径,需要用 deptId 反查回填,避免保存时丢组织
- this.restoreOrganization(row.deptId);
- return row;
- },
- async init(options) {
- uni.setNavigationBarTitle({ title: "转正申请模板" });
- await this.refreshOrganizations();
- try {
- this.categories = await loadOnlineTemplateCategories();
- } catch (error) {
- this.categories = [];
- }
- const id = options.id || "";
- if (!id) return;
- uni.showLoading({ title: "加载中..." });
- try {
- await this.loadTemplate(id);
- } catch (e) {
- uni.hideLoading();
- uni.showToast({ title: (e && e.message) || "加载失败", icon: "none" });
- setTimeout(() => this.onCancel(), 900);
- return;
- }
- uni.hideLoading();
- },
- },
- };
- </script>
- <style scoped lang="scss">
- @import "@/pages/hr/common-style.scss";
- .page-no-tab {
- padding-bottom: 170rpx;
- }
- .form-intro {
- padding: 30rpx 30rpx 22rpx;
- display: flex;
- align-items: center;
- background: #fff;
- }
- .form-intro > view:last-child {
- margin-left: 22rpx;
- }
- .intro-title {
- display: block;
- font-size: 32rpx;
- font-weight: 600;
- }
- .intro-sub {
- display: block;
- margin-top: 9rpx;
- color: #8b939f;
- font-size: 23rpx;
- }
- .form-card {
- overflow: hidden;
- }
- .notice {
- margin: 20rpx 24rpx 0;
- padding: 20rpx 24rpx;
- border-radius: 12rpx;
- background: #fff8e6;
- color: #a86b00;
- font-size: 23rpx;
- line-height: 1.6;
- }
- .field {
- position: relative;
- min-height: 108rpx;
- padding: 28rpx;
- display: flex;
- align-items: center;
- }
- .label {
- width: 200rpx;
- flex-shrink: 0;
- font-size: 28rpx;
- }
- .required::before {
- content: "*";
- color: #ee4d4d;
- margin-right: 6rpx;
- }
- .value {
- flex: 1;
- text-align: right;
- color: #1f2329;
- font-size: 27rpx;
- }
- .value.link {
- color: #157a2c;
- }
- .placeholder {
- flex: 1;
- text-align: right;
- color: #b2b7be;
- font-size: 27rpx;
- }
- .arrow {
- margin-left: 13rpx;
- color: #c4c8ce;
- font-size: 38rpx;
- }
- .text-input {
- flex: 1;
- height: 68rpx;
- text-align: right;
- font-size: 27rpx;
- color: #1f2329;
- }
- .textarea-field {
- display: block;
- min-height: 240rpx;
- }
- .textarea-field .label {
- display: block;
- width: auto;
- margin-bottom: 20rpx;
- }
- .textarea-field textarea {
- width: 100%;
- height: 180rpx;
- padding: 18rpx;
- border-radius: 12rpx;
- background: #f6f7f8;
- font-size: 25rpx;
- box-sizing: border-box;
- }
- .counter {
- display: block;
- margin-top: 10rpx;
- text-align: right;
- color: #b2b7be;
- font-size: 22rpx;
- }
- .hint {
- display: block;
- margin-top: 12rpx;
- color: #8b939f;
- font-size: 22rpx;
- }
- .footer-tip {
- padding: 8rpx 30rpx 0;
- font-size: 22rpx;
- }
- .bottom-action {
- position: fixed;
- z-index: 10;
- left: 0;
- right: 0;
- bottom: 0;
- height: 120rpx;
- padding: 16rpx 24rpx;
- background: #fff;
- border-top: 1rpx solid #e8e9eb;
- display: flex;
- gap: 20rpx;
- box-sizing: border-box;
- }
- .draft-btn {
- flex: 1;
- height: 84rpx;
- line-height: 84rpx;
- text-align: center;
- border-radius: 12rpx;
- background: #f5f6f7;
- color: #4a5058;
- font-size: 30rpx;
- }
- .submit-btn {
- flex: 2;
- height: 84rpx;
- line-height: 84rpx;
- text-align: center;
- border-radius: 12rpx;
- background: #157a2c;
- color: #fff;
- font-size: 30rpx;
- }
- @media (min-width: 800px) {
- .bottom-action {
- max-width: 520px;
- left: 50%;
- transform: translateX(-50%);
- }
- }
- </style>
|