index.vue 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. <template>
  2. <ContentWrap>
  3. <el-form
  4. class="-mb-15px"
  5. :model="queryParams"
  6. :inline="true"
  7. label-width="90px"
  8. >
  9. <TenantSelect
  10. v-model="queryParams.tenantIds"
  11. single
  12. :disabled="!canSwitchTenant"
  13. placeholder="请选择机构名称"
  14. prop="tenantIds"
  15. @change="handleQuery"
  16. />
  17. <el-form-item v-if="!canSwitchTenant">
  18. <span class="text-12px text-gray-500">当前账号已绑定院区,院区不可切换</span>
  19. </el-form-item>
  20. <el-form-item>
  21. <el-button type="primary" @click="handleQuery">
  22. <Icon icon="ep:search" class="mr-5px" /> 查询
  23. </el-button>
  24. </el-form-item>
  25. </el-form>
  26. </ContentWrap>
  27. <ContentWrap>
  28. <div class="mb-15px flex items-center justify-between">
  29. <div class="flex items-center">
  30. <span class="text-16px font-bold">预约须知内容</span>
  31. <span class="ml-10px text-12px text-gray-500">保存后小程序端将展示该内容</span>
  32. </div>
  33. <div>
  34. <el-button @click="handleQuery">
  35. <Icon icon="ep:refresh" class="mr-5px" /> 重新加载
  36. </el-button>
  37. <el-button type="primary" :loading="saveLoading" @click="handleSave">
  38. <Icon icon="ep:select" class="mr-5px" /> 保存
  39. </el-button>
  40. </div>
  41. </div>
  42. <el-input
  43. v-loading="loading"
  44. v-model="formData.content"
  45. type="textarea"
  46. :rows="18"
  47. maxlength="2000"
  48. show-word-limit
  49. placeholder="请输入须知内容,每条建议单独一行(换行即视为一条)"
  50. />
  51. </ContentWrap>
  52. <ContentWrap>
  53. <div class="mb-15px text-16px font-bold">小程序预览</div>
  54. <div class="preview-wrap">
  55. <div class="preview-title">预约须知</div>
  56. <div class="preview-content">
  57. <template v-if="previewLines.length">
  58. <p v-for="(line, index) in previewLines" :key="index" class="preview-line">
  59. {{ line }}
  60. </p>
  61. </template>
  62. <div v-else class="text-gray-400">暂无须知内容</div>
  63. </div>
  64. </div>
  65. </ContentWrap>
  66. </template>
  67. <script lang="ts" setup>
  68. import {
  69. createNoticeCustomer,
  70. getNoticeCustomerList,
  71. saveNoticeCustomer
  72. } from '@/api/elderly/elder/notice-customer'
  73. import { useUserStore } from '@/store/modules/user'
  74. defineOptions({ name: 'NoticeCustomer' })
  75. const userStore = useUserStore()
  76. const message = useMessage()
  77. /** 须知类型固定为预约须知(APPOINTMENT,VISIT 为后端固定文案) */
  78. const NOTICE_TYPE = 'APPOINTMENT' as const
  79. const loading = ref(false)
  80. const saveLoading = ref(false)
  81. const queryParams = reactive({
  82. tenantIds: userStore.orgTenantId || []
  83. })
  84. const formData = reactive({
  85. id: undefined as number | undefined,
  86. content: ''
  87. })
  88. // 预览:按换行拆分为多条
  89. const previewLines = computed(() =>
  90. (formData.content || '')
  91. .split('\n')
  92. .map((item) => item.trim())
  93. .filter((item) => item)
  94. )
  95. /** 当前选中院区 */
  96. const currentTenantId = computed(() => queryParams.tenantIds?.[0])
  97. /** 是否集团账号:集团账号可切换院区,普通院区账号固定为当前院区 */
  98. const canSwitchTenant = computed(() => !!userStore.getOrgTenant?.multiple)
  99. /** 内容换行处理:兼容后端存的字面量 \n */
  100. const normalizeContent = (content?: string) => (content || '').replace(/\\n/g, '\n')
  101. /** 按机构查询须知(返回第一条) */
  102. const fetchDetail = async () => {
  103. const list = await getNoticeCustomerList({
  104. type: NOTICE_TYPE,
  105. tenantId: currentTenantId.value
  106. })
  107. return list?.[0]
  108. }
  109. /**
  110. * 用初始化数据为当前机构创建须知
  111. * 后端未做初始化:机构下无数据时,不传 tenantId 再查一次拿到初始须知,
  112. * 自动创建到当前机构下
  113. */
  114. const initNotice = async () => {
  115. try {
  116. const defaultList = await getNoticeCustomerList({ type: NOTICE_TYPE, tenantId: 1 })
  117. // 初始数据可能返回多条,用 \n 拼接后展示
  118. const initContent = (defaultList || [])
  119. .map((item) => normalizeContent(item?.content))
  120. .filter((item) => item.trim())
  121. .join('\n')
  122. if (!initContent) {
  123. return false
  124. }
  125. await createNoticeCustomer({
  126. type: NOTICE_TYPE,
  127. tenantId: currentTenantId.value,
  128. content: initContent
  129. })
  130. return true
  131. } catch {
  132. return false
  133. }
  134. }
  135. /** 查询院区须知:无数据时先自动初始化,再查询回填 */
  136. const getDetail = async () => {
  137. if (!currentTenantId.value) {
  138. message.warning('请先选择机构')
  139. return
  140. }
  141. loading.value = true
  142. try {
  143. const detail = await fetchDetail()
  144. if (detail) {
  145. formData.id = detail.id
  146. formData.content = normalizeContent(detail.content)
  147. return
  148. }
  149. // 机构下无数据 -> 自动初始化后再查一次回填
  150. const created = await initNotice()
  151. if (created) {
  152. const newDetail = await fetchDetail()
  153. formData.id = newDetail?.id
  154. formData.content = normalizeContent(newDetail?.content)
  155. return
  156. }
  157. formData.id = undefined
  158. formData.content = ''
  159. } finally {
  160. loading.value = false
  161. }
  162. }
  163. /** 切换机构 / 查询 */
  164. const handleQuery = () => {
  165. getDetail()
  166. }
  167. /** 保存 */
  168. const handleSave = async () => {
  169. if (!currentTenantId.value) {
  170. message.warning('请先选择机构')
  171. return
  172. }
  173. if (!formData.content || !formData.content.trim()) {
  174. message.warning('须知内容不能为空')
  175. return
  176. }
  177. saveLoading.value = true
  178. try {
  179. await saveNoticeCustomer({
  180. id: formData.id,
  181. type: NOTICE_TYPE,
  182. tenantId: currentTenantId.value,
  183. content: formData.content
  184. })
  185. message.success('保存成功')
  186. await getDetail()
  187. } finally {
  188. saveLoading.value = false
  189. }
  190. }
  191. /** 初始化 */
  192. onMounted(() => {
  193. getDetail()
  194. })
  195. </script>
  196. <style lang="scss" scoped>
  197. .preview-wrap {
  198. width: 320px;
  199. padding: 15px;
  200. background: #fafafa;
  201. border: 1px solid #e4e7ed;
  202. border-radius: 8px;
  203. }
  204. .preview-title {
  205. margin-bottom: 10px;
  206. font-weight: bold;
  207. text-align: center;
  208. }
  209. .preview-line {
  210. margin: 6px 0;
  211. font-size: 13px;
  212. line-height: 1.6;
  213. color: #333;
  214. }
  215. </style>