|
@@ -0,0 +1,673 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <view class="page-no-tab">
|
|
|
|
|
+ <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>
|
|
|
|
|
+ <view class="field press" @click="chooseCategory">
|
|
|
|
|
+ <text class="label required">模板分类</text>
|
|
|
|
|
+ <text :class="categoryLabel ? 'value' : 'placeholder'">{{ categoryLabel || '请选择' }}</text>
|
|
|
|
|
+ <text class="arrow">›</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="divider"></view>
|
|
|
|
|
+ <view class="field press" @click="chooseLegal">
|
|
|
|
|
+ <text class="label required">所属法人</text>
|
|
|
|
|
+ <text :class="form.legalName ? 'value' : 'placeholder'">{{ form.legalName || '请选择' }}</text>
|
|
|
|
|
+ <text class="arrow">›</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="divider"></view>
|
|
|
|
|
+ <view class="field press" @click="onDeptFieldTap">
|
|
|
|
|
+ <text class="label required">所属组织</text>
|
|
|
|
|
+ <picker
|
|
|
|
|
+ v-if="form.legalId"
|
|
|
|
|
+ mode="multiSelector"
|
|
|
|
|
+ :range="deptColumns"
|
|
|
|
|
+ :value="deptIndex"
|
|
|
|
|
+ range-key="name"
|
|
|
|
|
+ @columnchange="onDeptColChange"
|
|
|
|
|
+ @change="onDeptPick"
|
|
|
|
|
+ >
|
|
|
|
|
+ <text :class="form.deptPath ? 'value' : 'placeholder'">{{ form.deptPath || '请选择' }}</text>
|
|
|
|
|
+ </picker>
|
|
|
|
|
+ <text v-else class="placeholder">请先选择法人</text>
|
|
|
|
|
+ <text class="arrow">›</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <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"
|
|
|
|
|
+ maxlength="-1"
|
|
|
|
|
+ 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"
|
|
|
|
|
+ maxlength="-1"
|
|
|
|
|
+ placeholder="变量定义 JSON 数组,选填"
|
|
|
|
|
+ />
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <view class="section-title"><text>画布预览</text></view>
|
|
|
|
|
+ <view class="form-card card section">
|
|
|
|
|
+ <A4Preview :content="form.documentContent" />
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <template 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>
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <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 setup>
|
|
|
|
|
+import { computed, reactive, ref } from 'vue'
|
|
|
|
|
+import { onLoad } from '@dcloudio/uni-app'
|
|
|
|
|
+import A4Preview from '@/components/A4Preview.vue'
|
|
|
|
|
+import ModuleIcon from '@/components/ModuleIcon.vue'
|
|
|
|
|
+import { useAuthGuard } from '@/hooks/useAuthGuard'
|
|
|
|
|
+import { applicationTypes } from '@/data/mock'
|
|
|
|
|
+import { back } from '@/utils/router'
|
|
|
|
|
+import { getServerConfig } from '@/utils/auth'
|
|
|
|
|
+import { listOrganizations } from '@/api/organization'
|
|
|
|
|
+import { buildDeptTreeUnderLegal, pickLegalEntities } from '@/utils/organization-tree'
|
|
|
|
|
+import {
|
|
|
|
|
+ TEMPLATE_STATUS_LABELS,
|
|
|
|
|
+ loadOnlineTemplateCategories,
|
|
|
|
|
+ loadOnlineTemplateDetail,
|
|
|
|
|
+ publishOnlineTemplateVersion,
|
|
|
|
|
+ saveOnlineTemplate,
|
|
|
|
|
+} from '@/api/onlineDocument'
|
|
|
|
|
+
|
|
|
|
|
+useAuthGuard()
|
|
|
|
|
+const meta = applicationTypes.regular || { title: '转正申请', color: '#269a99' }
|
|
|
|
|
+
|
|
|
|
|
+const form = reactive({
|
|
|
|
|
+ id: '',
|
|
|
|
|
+ templateName: '',
|
|
|
|
|
+ templateCode: '',
|
|
|
|
|
+ categoryId: '',
|
|
|
|
|
+ legalId: '',
|
|
|
|
|
+ legalName: '',
|
|
|
|
|
+ deptId: '',
|
|
|
|
|
+ deptPath: '',
|
|
|
|
|
+ remark: '',
|
|
|
|
|
+ documentContent: '',
|
|
|
|
|
+ variablesJson: '',
|
|
|
|
|
+ templateStatus: '',
|
|
|
|
|
+ templateStatusLabel: '',
|
|
|
|
|
+ versionStatus: '',
|
|
|
|
|
+ currentVersionNo: 1,
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const orgList = ref([])
|
|
|
|
|
+const companies = ref([])
|
|
|
|
|
+const deptTree = ref([])
|
|
|
|
|
+const deptIndex = ref([0, 0, 0])
|
|
|
|
|
+const categories = ref([])
|
|
|
|
|
+const versionChangeRemark = ref('')
|
|
|
|
|
+const submitting = ref(false)
|
|
|
|
|
+
|
|
|
|
|
+/** 只有草稿模板允许直接改正文 / 变量 / 分类,其余要走「发布新版本」 */
|
|
|
|
|
+const isDraft = computed(() => !form.id || form.templateStatus === 'DRAFT')
|
|
|
|
|
+
|
|
|
|
|
+const introSub = computed(() =>
|
|
|
|
|
+ form.id
|
|
|
|
|
+ ? `${form.templateStatusLabel || TEMPLATE_STATUS_LABELS[form.templateStatus] || ''} · 当前版本 V${form.currentVersionNo || 1}`
|
|
|
|
|
+ : '线上 Word 模板,供转正申请选用',
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+const submitLabel = computed(() => {
|
|
|
|
|
+ if (!form.id) return '创建模板'
|
|
|
|
|
+ return isDraft.value ? '保存修改' : '发布新版本'
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const contentHint = computed(() =>
|
|
|
|
|
+ isDraft.value
|
|
|
|
|
+ ? '变量用双花括号占位,例如 {{EMPLOYEE_NAME}}'
|
|
|
|
|
+ : '已启用 / 停用模板:改完正文点底部「发布新版本」生效',
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+const footerTip = computed(() =>
|
|
|
|
|
+ getServerConfig().mode === 'server'
|
|
|
|
|
+ ? '保存走 /hr/online-documents/templates,新建后默认为停用,需手动启用'
|
|
|
|
|
+ : '演示模式:模板保存在本机,操作不会同步到服务器',
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+const categoryLabel = computed(() => {
|
|
|
|
|
+ if (!form.categoryId) return ''
|
|
|
|
|
+ const hit = categories.value.find((item) => String(item.id) === String(form.categoryId))
|
|
|
|
|
+ return hit ? hit.categoryName : `分类 ${form.categoryId}`
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const deptColumns = computed(() => {
|
|
|
|
|
+ const t = deptTree.value
|
|
|
|
|
+ const l1 = t.map((n) => ({ name: n.name, id: n.id }))
|
|
|
|
|
+ const l2 =
|
|
|
|
|
+ l1.length && deptIndex.value[0] < l1.length
|
|
|
|
|
+ ? (t[deptIndex.value[0]].children || []).map((n) => ({ name: n.name, id: n.id }))
|
|
|
|
|
+ : []
|
|
|
|
|
+ const l3 =
|
|
|
|
|
+ l2.length && deptIndex.value[1] < l2.length
|
|
|
|
|
+ ? (t[deptIndex.value[0]].children[deptIndex.value[1]].children || []).map((n) => ({
|
|
|
|
|
+ name: n.name,
|
|
|
|
|
+ id: n.id,
|
|
|
|
|
+ }))
|
|
|
|
|
+ : []
|
|
|
|
|
+ return [l1, l2, l3]
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+function validate() {
|
|
|
|
|
+ if (!form.templateName.trim()) return '请填写模板名称'
|
|
|
|
|
+ if (!form.templateCode.trim()) return '请填写模板编码'
|
|
|
|
|
+ if (!form.categoryId) return '请选择模板分类'
|
|
|
|
|
+ if (!form.deptId) return '请选择所属组织'
|
|
|
|
|
+ if (!form.documentContent.trim()) return '请填写模板正文'
|
|
|
|
|
+ const variables = form.variablesJson.trim()
|
|
|
|
|
+ if (variables) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const parsed = JSON.parse(variables)
|
|
|
|
|
+ if (!Array.isArray(parsed)) return '变量定义需为 JSON 数组'
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ return '变量定义不是合法 JSON'
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return ''
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function refreshOrganizations() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ orgList.value = await listOrganizations()
|
|
|
|
|
+ companies.value = pickLegalEntities(orgList.value)
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ orgList.value = []
|
|
|
|
|
+ companies.value = []
|
|
|
|
|
+ uni.showToast({ title: '组织列表加载失败', icon: 'none' })
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function refreshDeptTree(legalId) {
|
|
|
|
|
+ deptTree.value = legalId ? buildDeptTreeUnderLegal(orgList.value, legalId) : []
|
|
|
|
|
+ deptIndex.value = [0, 0, 0]
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** 在组织树里定位 deptId 所在的三级索引,用于回填 picker */
|
|
|
|
|
+function findDeptIndex(tree = [], deptId) {
|
|
|
|
|
+ const key = String(deptId || '')
|
|
|
|
|
+ for (let i = 0; i < tree.length; i += 1) {
|
|
|
|
|
+ if (String(tree[i].id) === key) return [i, 0, 0]
|
|
|
|
|
+ const level2 = tree[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 反查所属法人与组织路径(编辑态回填,避免保存时丢组织) */
|
|
|
|
|
+function restoreOrganization(deptId) {
|
|
|
|
|
+ form.legalId = ''
|
|
|
|
|
+ form.legalName = ''
|
|
|
|
|
+ form.deptPath = ''
|
|
|
|
|
+ form.deptId = deptId ? String(deptId) : ''
|
|
|
|
|
+ if (!form.deptId) return
|
|
|
|
|
+ const byId = new Map(orgList.value.map((item) => [String(item.id), item]))
|
|
|
|
|
+ const node = byId.get(form.deptId)
|
|
|
|
|
+ // 组织列表里查不到(跨法人 / 已停用组织)时保留原主键,只做提示
|
|
|
|
|
+ if (!node) {
|
|
|
|
|
+ form.deptPath = `组织 ${form.deptId}`
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ const isCompany = (item) =>
|
|
|
|
|
+ companies.value.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.find(isCompany)
|
|
|
|
|
+ const deptNodes = chain.filter((item) => !isCompany(item))
|
|
|
|
|
+ const tail = deptNodes.length ? deptNodes : chain
|
|
|
|
|
+ form.deptId = tail[tail.length - 1].id
|
|
|
|
|
+ form.deptPath = tail.map((item) => item.name).filter(Boolean).join(' / ')
|
|
|
|
|
+ if (!legal) return
|
|
|
|
|
+ form.legalId = legal.id
|
|
|
|
|
+ form.legalName = legal.name
|
|
|
|
|
+ refreshDeptTree(legal.id)
|
|
|
|
|
+ deptIndex.value = findDeptIndex(deptTree.value, form.deptId)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function chooseCategory() {
|
|
|
|
|
+ if (!isDraft.value) {
|
|
|
|
|
+ uni.showToast({ title: '已启用 / 停用模板不可改分类,请发布新版本', icon: 'none' })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ const options = categories.value
|
|
|
|
|
+ if (!options.length) {
|
|
|
|
|
+ uni.showModal({
|
|
|
|
|
+ title: '模板分类',
|
|
|
|
|
+ content: '后端暂未提供分类列表接口,请直接填写分类主键(数字)',
|
|
|
|
|
+ showCancel: false,
|
|
|
|
|
+ success: () => promptCategoryId(),
|
|
|
|
|
+ })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ const itemList = [...options.map((item) => item.categoryName), '手动输入分类主键']
|
|
|
|
|
+ uni.showActionSheet({
|
|
|
|
|
+ itemList,
|
|
|
|
|
+ success: (r) => {
|
|
|
|
|
+ if (r.tapIndex === options.length) {
|
|
|
|
|
+ promptCategoryId()
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ const picked = options[r.tapIndex]
|
|
|
|
|
+ if (picked) form.categoryId = String(picked.id)
|
|
|
|
|
+ },
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function promptCategoryId() {
|
|
|
|
|
+ uni.showModal({
|
|
|
|
|
+ title: '输入分类主键',
|
|
|
|
|
+ editable: true,
|
|
|
|
|
+ placeholderText: '请输入模板分类主键(数字)',
|
|
|
|
|
+ success: (r) => {
|
|
|
|
|
+ if (!r.confirm) return
|
|
|
|
|
+ const text = String(r.content || '').trim()
|
|
|
|
|
+ if (!text) return
|
|
|
|
|
+ form.categoryId = text
|
|
|
|
|
+ },
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function chooseLegal() {
|
|
|
|
|
+ if (!companies.value.length) {
|
|
|
|
|
+ uni.showToast({ title: '法人列表为空', icon: 'none' })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ uni.showActionSheet({
|
|
|
|
|
+ itemList: companies.value.map((item) => item.name),
|
|
|
|
|
+ success: (r) => {
|
|
|
|
|
+ const picked = companies.value[r.tapIndex]
|
|
|
|
|
+ if (!picked) return
|
|
|
|
|
+ form.legalId = picked.id
|
|
|
|
|
+ form.legalName = picked.name
|
|
|
|
|
+ form.deptId = ''
|
|
|
|
|
+ form.deptPath = ''
|
|
|
|
|
+ refreshDeptTree(picked.id)
|
|
|
|
|
+ },
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function onDeptFieldTap() {
|
|
|
|
|
+ if (!form.legalId) {
|
|
|
|
|
+ uni.showToast({ title: '请先选择法人', icon: 'none' })
|
|
|
|
|
+ chooseLegal()
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function onDeptColChange(e) {
|
|
|
|
|
+ const next = [...deptIndex.value]
|
|
|
|
|
+ next[e.detail.column] = e.detail.value
|
|
|
|
|
+ if (e.detail.column === 0) {
|
|
|
|
|
+ next[1] = 0
|
|
|
|
|
+ next[2] = 0
|
|
|
|
|
+ }
|
|
|
|
|
+ if (e.detail.column === 1) next[2] = 0
|
|
|
|
|
+ deptIndex.value = next
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function onDeptPick(e) {
|
|
|
|
|
+ const [i0, i1, i2] = e.detail.value
|
|
|
|
|
+ const l1 = deptTree.value[i0]
|
|
|
|
|
+ const l2 = l1?.children?.[i1]
|
|
|
|
|
+ const l3 = l2?.children?.[i2]
|
|
|
|
|
+ const nodes = [l2, l3].filter(Boolean)
|
|
|
|
|
+ if (!nodes.length) {
|
|
|
|
|
+ form.deptId = ''
|
|
|
|
|
+ form.deptPath = ''
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ form.deptId = nodes[nodes.length - 1].id
|
|
|
|
|
+ form.deptPath = nodes.map((n) => n.name).filter(Boolean).join(' / ')
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function onSave() {
|
|
|
|
|
+ if (submitting.value) return
|
|
|
|
|
+ const error = validate()
|
|
|
|
|
+ if (error) {
|
|
|
|
|
+ uni.showToast({ title: error, icon: 'none' })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ submitting.value = true
|
|
|
|
|
+ uni.showLoading({ title: '保存中...' })
|
|
|
|
|
+ try {
|
|
|
|
|
+ await saveOnlineTemplate({ form: { ...form }, existingId: form.id || null })
|
|
|
|
|
+ uni.hideLoading()
|
|
|
|
|
+ uni.showToast({ title: form.id ? '已保存' : '模板已创建', icon: 'success' })
|
|
|
|
|
+ setTimeout(() => back('/pages/manage/regular-template-list'), 800)
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ uni.hideLoading()
|
|
|
|
|
+ uni.showToast({ title: e.message || '保存失败', icon: 'none' })
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ submitting.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** 底部主按钮:草稿直接保存,已启用 / 停用走发布新版本 */
|
|
|
|
|
+function onSubmit() {
|
|
|
|
|
+ if (submitting.value) return
|
|
|
|
|
+ if (isDraft.value) return onSave()
|
|
|
|
|
+ return onPublishVersion()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function onPublishVersion() {
|
|
|
|
|
+ if (!form.id) return
|
|
|
|
|
+ if (!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(form.id, {
|
|
|
|
|
+ documentContent: form.documentContent,
|
|
|
|
|
+ variablesJson: form.variablesJson,
|
|
|
|
|
+ changeRemark: versionChangeRemark.value,
|
|
|
|
|
+ })
|
|
|
|
|
+ uni.hideLoading()
|
|
|
|
|
+ versionChangeRemark.value = ''
|
|
|
|
|
+ // 发布新版本会自动停用模板并生成草稿版本,重新拉一次详情同步状态
|
|
|
|
|
+ await loadTemplate(form.id)
|
|
|
|
|
+ uni.showToast({ title: '新版本已发布', icon: 'success' })
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ uni.hideLoading()
|
|
|
|
|
+ uni.showToast({ title: e.message || '发布失败', icon: 'none' })
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function onCancel() {
|
|
|
|
|
+ back('/pages/manage/regular-template-list')
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** 拉取模板详情并回填表单(编辑进入与发布新版本后复用) */
|
|
|
|
|
+async function loadTemplate(id) {
|
|
|
|
|
+ const row = await loadOnlineTemplateDetail(id)
|
|
|
|
|
+ if (!row) throw new Error('未找到该模板')
|
|
|
|
|
+ Object.assign(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 反查回填,避免保存时丢组织
|
|
|
|
|
+ restoreOrganization(row.deptId)
|
|
|
|
|
+ return row
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+onLoad(async (options) => {
|
|
|
|
|
+ uni.setNavigationBarTitle({ title: '转正申请模板' })
|
|
|
|
|
+ await refreshOrganizations()
|
|
|
|
|
+ try {
|
|
|
|
|
+ categories.value = await loadOnlineTemplateCategories()
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ categories.value = []
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const id = options?.id || ''
|
|
|
|
|
+ if (!id) return
|
|
|
|
|
+ uni.showLoading({ title: '加载中...' })
|
|
|
|
|
+ try {
|
|
|
|
|
+ await loadTemplate(id)
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ uni.hideLoading()
|
|
|
|
|
+ uni.showToast({ title: e.message || '加载失败', icon: 'none' })
|
|
|
|
|
+ setTimeout(() => back('/pages/manage/regular-template-list'), 900)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ uni.hideLoading()
|
|
|
|
|
+})
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped>
|
|
|
|
|
+.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: #1677ff;
|
|
|
|
|
+}
|
|
|
|
|
+.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;
|
|
|
|
|
+}
|
|
|
|
|
+.textarea-field textarea[disabled] {
|
|
|
|
|
+ color: #8b939f;
|
|
|
|
|
+ background: #f2f3f5;
|
|
|
|
|
+}
|
|
|
|
|
+.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: #1677ff;
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ font-size: 30rpx;
|
|
|
|
|
+}
|
|
|
|
|
+@media (min-width: 800px) {
|
|
|
|
|
+ .bottom-action {
|
|
|
|
|
+ max-width: 520px;
|
|
|
|
|
+ left: 50%;
|
|
|
|
|
+ transform: translateX(-50%);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|