index.vue 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. <template>
  2. <div class="app-container sys-config-page">
  3. <el-card class="config-card" shadow="hover">
  4. <!-- 主配置选项卡 -->
  5. <el-tabs v-model="activeMainTab" class="main-tabs">
  6. <el-tab-pane label="网站设置" name="website" />
  7. <el-tab-pane label="平台配置" name="platform" />
  8. <el-tab-pane label="文件存储配置" name="storage">
  9. <div class="storage-container animated fadeIn">
  10. <storage-config v-if="activeMainTab === 'storage'" />
  11. </div>
  12. </el-tab-pane>
  13. <el-tab-pane label="短信配置" name="sms" />
  14. <el-tab-pane label="支付配置" name="payment">
  15. <div class="agreement-container animated fadeIn">
  16. <wxpay-config v-if="activeMainTab === 'payment'" />
  17. </div>
  18. </el-tab-pane>
  19. <el-tab-pane label="协议配置" name="agreement">
  20. <div class="agreement-container animated fadeIn">
  21. <!-- 子配置选项卡:协议类型 -->
  22. <div class="sub-tabs-wrapper">
  23. <div
  24. v-for="tab in agreementSubTabs"
  25. :key="tab.name"
  26. :class="['sub-tab-item', activeSubTab === tab.name ? 'active' : '']"
  27. @click="activeSubTab = tab.name"
  28. >
  29. {{ tab.label }}
  30. </div>
  31. </div>
  32. <!-- 协议编辑表单 -->
  33. <el-form :model="form" label-width="100px" class="config-form">
  34. <el-form-item label="协议标题" required>
  35. <el-input
  36. v-model="form.title"
  37. placeholder="请输入协议标题"
  38. class="title-input"
  39. />
  40. </el-form-item>
  41. <el-form-item label="协议内容" required>
  42. <Editor v-model="form.content" :min-height="400" />
  43. </el-form-item>
  44. <el-form-item class="form-actions">
  45. <el-button type="primary" size="large" icon="Check" @click="handleSave">
  46. 保存并应用
  47. </el-button>
  48. </el-form-item>
  49. </el-form>
  50. </div>
  51. </el-tab-pane>
  52. </el-tabs>
  53. </el-card>
  54. <!-- 其他Tab的占位信息 (非协议配置、支付配置和文件存储配置时显示) -->
  55. <el-empty v-if="activeMainTab !== 'agreement' && activeMainTab !== 'payment' && activeMainTab !== 'storage'" description="该模块配置开发中..." />
  56. </div>
  57. </template>
  58. <script setup name="SysConfig">
  59. import { ref, reactive, watch, onMounted } from 'vue';
  60. import Editor from '@/components/Editor/index.vue';
  61. import WxpayConfig from './wxpay.vue';
  62. import StorageConfig from './storage.vue';
  63. import { ElMessage } from 'element-plus';
  64. import request from '@/utils/request';
  65. // 主选项卡
  66. const activeMainTab = ref('agreement');
  67. // 表单数据
  68. const form = reactive({
  69. title: '用户服务协议',
  70. content: ''
  71. });
  72. // 当前协议对应的数据库 ID
  73. const currentAgreementId = ref(null);
  74. // 协议子选项卡
  75. const activeSubTab = ref('user');
  76. const agreementSubTabs = [
  77. { label: '用户协议', name: 'user' },
  78. { label: '隐私政策', name: 'privacy' },
  79. { label: 'Offer政策', name: 'offer' }
  80. ];
  81. const activeSubTabsMap = {
  82. user: '用户协议',
  83. privacy: '隐私政策',
  84. offer: 'Offer政策'
  85. };
  86. const agreementTypeMap = {
  87. user: 'service',
  88. privacy: 'privacy',
  89. offer: 'offer'
  90. };
  91. // 获取协议内容
  92. const fetchAgreement = async () => {
  93. try {
  94. const typeKey = agreementTypeMap[activeSubTab.value];
  95. const res = await request({
  96. url: `/miniapp/auth/agreement?type=${typeKey}`,
  97. method: 'get'
  98. });
  99. if (res && res.code === 200 && res.data) {
  100. currentAgreementId.value = res.data.id;
  101. form.title = res.data.title || activeSubTabsMap[activeSubTab.value];
  102. form.content = res.data.content || '';
  103. } else {
  104. currentAgreementId.value = null;
  105. form.title = activeSubTabsMap[activeSubTab.value];
  106. form.content = '';
  107. }
  108. } catch (error) {
  109. console.error('获取协议失败', error);
  110. }
  111. };
  112. // 切换 Tab 时重新抓取对应数据
  113. watch(activeSubTab, () => {
  114. fetchAgreement();
  115. });
  116. // 挂载时抓取初始数据
  117. onMounted(() => {
  118. fetchAgreement();
  119. });
  120. /** 保存配置 */
  121. const handleSave = async () => {
  122. if (!form.title) {
  123. return ElMessage.warning('请输入协议标题');
  124. }
  125. if (!form.content || form.content === '<p></p>') {
  126. return ElMessage.warning('请输入协议内容');
  127. }
  128. try {
  129. const typeKey = agreementTypeMap[activeSubTab.value];
  130. const payload = {
  131. id: currentAgreementId.value,
  132. type: typeKey,
  133. title: form.title,
  134. content: form.content
  135. };
  136. // 调用后端的 /edit 接口
  137. const res = await request({
  138. url: '/miniapp/auth/edit',
  139. method: 'post',
  140. data: payload
  141. });
  142. if (res.code === 200) {
  143. ElMessage.success({
  144. message: `${activeSubTabsMap[activeSubTab.value]} 保存成功`,
  145. type: 'success',
  146. duration: 2000
  147. });
  148. fetchAgreement(); // 重新拉取
  149. } else {
  150. ElMessage.error(res.msg || '保存失败');
  151. }
  152. } catch (error) {
  153. console.error('保存协议失败', error);
  154. }
  155. };
  156. </script>
  157. <style scoped lang="scss">
  158. .sys-config-page {
  159. padding: 20px;
  160. background-color: #f5f7fa;
  161. min-height: calc(100vh - 84px);
  162. .config-card {
  163. border-radius: 12px;
  164. border: none;
  165. box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
  166. :deep(.el-card__body) {
  167. padding: 0;
  168. }
  169. }
  170. /* 主选项卡样式定制 */
  171. .main-tabs {
  172. :deep(.el-tabs__header) {
  173. margin-bottom: 0;
  174. padding: 10px 20px 0;
  175. background: #fff;
  176. border-radius: 12px 12px 0 0;
  177. }
  178. :deep(.el-tabs__nav-wrap::after) {
  179. height: 1px;
  180. background-color: #ebeef5;
  181. }
  182. :deep(.el-tabs__item) {
  183. font-size: 15px;
  184. font-weight: 500;
  185. height: 50px;
  186. line-height: 50px;
  187. transition: all 0.3s;
  188. &:hover {
  189. color: #409eff;
  190. }
  191. &.is-active {
  192. font-weight: 700;
  193. }
  194. }
  195. :deep(.el-tabs__content) {
  196. padding: 24px;
  197. background: #fff;
  198. border-radius: 0 0 12px 12px;
  199. }
  200. }
  201. .agreement-container {
  202. .sub-tabs-wrapper {
  203. display: flex;
  204. gap: 12px;
  205. margin-bottom: 30px;
  206. .sub-tab-item {
  207. padding: 8px 24px;
  208. border-radius: 6px;
  209. background: #f4f4f5;
  210. color: #606266;
  211. cursor: pointer;
  212. font-size: 14px;
  213. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  214. border: 1px solid transparent;
  215. &:hover {
  216. color: #409eff;
  217. background: #ecf5ff;
  218. }
  219. &.active {
  220. background: #409eff;
  221. color: #fff;
  222. box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
  223. transform: translateY(-1px);
  224. }
  225. }
  226. }
  227. .config-form {
  228. max-width: 1000px;
  229. .title-input {
  230. :deep(.el-input__wrapper) {
  231. box-shadow: 0 0 0 1px #dcdfe6 inset;
  232. &:hover {
  233. box-shadow: 0 0 0 1px #409eff inset;
  234. }
  235. &.is-focus {
  236. box-shadow: 0 0 0 1px #409eff inset;
  237. }
  238. }
  239. }
  240. :deep(.el-form-item__label) {
  241. font-weight: 600;
  242. color: #303133;
  243. &::before {
  244. margin-right: 4px;
  245. }
  246. }
  247. .form-actions {
  248. margin-top: 40px;
  249. .el-button {
  250. padding: 12px 40px;
  251. font-weight: 600;
  252. letter-spacing: 1px;
  253. border-radius: 8px;
  254. }
  255. }
  256. }
  257. }
  258. }
  259. /* 动画 */
  260. .fadeIn {
  261. animation: fadeIn 0.5s ease-in-out;
  262. }
  263. @keyframes fadeIn {
  264. from {
  265. opacity: 0;
  266. transform: translateY(10px);
  267. }
  268. to {
  269. opacity: 1;
  270. transform: translateY(0);
  271. }
  272. }
  273. </style>