Просмотр исходного кода

Merge branch 'main' of http://47.100.2.119:3000/imengyu/wenbao-website into main

快乐的梦鱼 недель назад: 4
Родитель
Сommit
f5ac0c297e
54 измененных файлов с 1088 добавлено и 237 удалено
  1. 1 1
      README.md
  2. 33 14
      package-lock.json
  3. 2 1
      package.json
  4. 92 0
      scripts/UpdateScript/app.mjs
  5. 53 0
      scripts/UpdateScript/auth.mjs
  6. 6 0
      scripts/UpdateScript/index.mjs
  7. 1 0
      server/api/ecms/article/[id].ts
  8. 32 14
      server/api/ecms/article/byChannelName.ts
  9. 7 3
      server/api/ecms/channel/[id].ts
  10. 3 0
      server/api/ecms/channel/nav.ts
  11. 2 0
      server/api/ecms/channel/sitemap.ts
  12. 2 1
      src/App.vue
  13. 2 2
      src/api/RequestModules.ts
  14. BIN
      src/assets/images/banner/1.jpg
  15. BIN
      src/assets/images/banner/2.jpg
  16. BIN
      src/assets/images/bars/1.jpg
  17. BIN
      src/assets/images/bars/2.jpg
  18. BIN
      src/assets/images/bars/3.jpg
  19. BIN
      src/assets/images/bars/4.jpg
  20. BIN
      src/assets/images/bars/5.jpg
  21. BIN
      src/assets/images/bars/6.jpg
  22. BIN
      src/assets/images/bars/7.jpg
  23. BIN
      src/assets/images/bars/8.jpg
  24. BIN
      src/assets/images/index-title.png
  25. BIN
      src/assets/images/juzhen.jpg
  26. 9 0
      src/assets/images/link-infos/icons/dy.svg
  27. 13 0
      src/assets/images/link-infos/icons/gzhh.svg
  28. 12 0
      src/assets/images/link-infos/icons/sph.svg
  29. 14 0
      src/assets/images/link-infos/icons/xhs.svg
  30. BIN
      src/assets/images/link-infos/qrcode/dy.jpg
  31. BIN
      src/assets/images/link-infos/qrcode/gzhh.jpg
  32. BIN
      src/assets/images/link-infos/qrcode/mn.png
  33. BIN
      src/assets/images/link-infos/qrcode/sph.jpg
  34. BIN
      src/assets/images/link-infos/qrcode/xhs.jpg
  35. BIN
      src/assets/images/qr1/activity.png
  36. BIN
      src/assets/images/qr1/volunteer.png
  37. BIN
      src/assets/images/xuexi.png
  38. 117 9
      src/assets/scss/main.scss
  39. 71 31
      src/components/Footer.vue
  40. 171 0
      src/components/LinkInfos.vue
  41. 57 9
      src/components/NavBar.vue
  42. 1 1
      src/components/Sidebar.vue
  43. 30 23
      src/components/content/CommonListPage.vue
  44. 15 0
      src/components/link/PageLink.vue
  45. 22 0
      src/composeable/WindowSizeListener.ts
  46. 23 3
      src/pages/about.vue
  47. 3 3
      src/pages/channel/[id].vue
  48. 6 12
      src/pages/channel/laws.vue
  49. 245 85
      src/pages/index.vue
  50. 2 0
      src/pages/inheritor/artifact.vue
  51. 2 0
      src/pages/inheritor/inheritor.vue
  52. 29 23
      src/pages/inheritor/intangible.vue
  53. 2 0
      src/pages/inheritor/seminar.vue
  54. 8 2
      src/pages/page/[id].vue

+ 1 - 1
README.md

@@ -1,6 +1,6 @@
 # wenbao-website
 
-[文保中心官网](https://xmswhycbhzx.cn/)前端项目。
+[文保中心官网](https://www.xmswhycbhzx.cn/)前端项目。
 
 ## 技术栈
 

+ 33 - 14
package-lock.json

@@ -9,7 +9,7 @@
       "version": "0.0.0",
       "dependencies": {
         "@ant-design-vue/nuxt": "^1.4.6",
-        "@imengyu/imengyu-utils": "^0.0.26",
+        "@imengyu/imengyu-utils": "^0.0.28",
         "@imengyu/js-request-transform": "^0.4.0",
         "@imengyu/vue-dynamic-form": "^0.1.1",
         "@imengyu/vue-scroll-rect": "^0.1.8",
@@ -27,6 +27,7 @@
         "mysql2": "^3.16.0",
         "nuxt": "^4.2.2",
         "pinia": "^3.0.1",
+        "tailwindcss-primeui": "^0.6.1",
         "tslib": "^2.8.1",
         "vue": "^3.5.13",
         "vue-router": "^4.5.0",
@@ -1198,21 +1199,12 @@
       }
     },
     "node_modules/@imengyu/imengyu-utils": {
-      "version": "0.0.26",
-      "resolved": "https://registry.npmmirror.com/@imengyu/imengyu-utils/-/imengyu-utils-0.0.26.tgz",
-      "integrity": "sha512-cveTHptpu5ISLgIBroY/Ke+YZOE2lwybfq0HdPVx+vvttMzEhSkejesYTWF2FKY+5hOwYmWmOglutS143DUBFA==",
+      "version": "0.0.28",
+      "resolved": "https://registry.npmmirror.com/@imengyu/imengyu-utils/-/imengyu-utils-0.0.28.tgz",
+      "integrity": "sha512-DE0hrBJjdAsyiNJER/ZS8cuEl9+nWZtyUiJBKFimxuVLLiBtPZC2PQRcrHBDdyhxeQHUhfjCvqRbbFjDY6jloQ==",
       "license": "MIT",
       "dependencies": {
-        "@imengyu/js-request-transform": "^0.3.6"
-      }
-    },
-    "node_modules/@imengyu/imengyu-utils/node_modules/@imengyu/js-request-transform": {
-      "version": "0.3.10",
-      "resolved": "https://registry.npmmirror.com/@imengyu/js-request-transform/-/js-request-transform-0.3.10.tgz",
-      "integrity": "sha512-OpzBZvLOB5n9cIbMDibxD2VHgj0HISRty0q/ob7/drYXXz/yQHE40cTwTH2BaUNXOZlkTGplW9OPgXGupbnRhw==",
-      "license": "MIT",
-      "dependencies": {
-        "dayjs": "^1.11.7"
+        "@imengyu/js-request-transform": "^0.4.0"
       }
     },
     "node_modules/@imengyu/js-request-transform": {
@@ -1937,6 +1929,17 @@
         }
       }
     },
+    "node_modules/@nuxt/cli/node_modules/commander": {
+      "version": "13.1.0",
+      "resolved": "https://registry.npmmirror.com/commander/-/commander-13.1.0.tgz",
+      "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==",
+      "license": "MIT",
+      "optional": true,
+      "peer": true,
+      "engines": {
+        "node": ">=18"
+      }
+    },
     "node_modules/@nuxt/cli/node_modules/perfect-debounce": {
       "version": "2.0.0",
       "resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-2.0.0.tgz",
@@ -12300,6 +12303,22 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/tailwindcss": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-4.2.1.tgz",
+      "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==",
+      "license": "MIT",
+      "peer": true
+    },
+    "node_modules/tailwindcss-primeui": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmmirror.com/tailwindcss-primeui/-/tailwindcss-primeui-0.6.1.tgz",
+      "integrity": "sha512-T69Rylcrmnt8zy9ik+qZvsLuRIrS9/k6rYJSIgZ1trnbEzGDDQSCIdmfyZknevqiHwpSJHSmQ9XT2C+S/hJY4A==",
+      "license": "MIT",
+      "peerDependencies": {
+        "tailwindcss": ">=3.1.0"
+      }
+    },
     "node_modules/tar": {
       "version": "7.5.2",
       "resolved": "https://registry.npmmirror.com/tar/-/tar-7.5.2.tgz",

+ 2 - 1
package.json

@@ -15,7 +15,7 @@
   },
   "dependencies": {
     "@ant-design-vue/nuxt": "^1.4.6",
-    "@imengyu/imengyu-utils": "^0.0.26",
+    "@imengyu/imengyu-utils": "^0.0.28",
     "@imengyu/js-request-transform": "^0.4.0",
     "@imengyu/vue-dynamic-form": "^0.1.1",
     "@imengyu/vue-scroll-rect": "^0.1.8",
@@ -33,6 +33,7 @@
     "mysql2": "^3.16.0",
     "nuxt": "^4.2.2",
     "pinia": "^3.0.1",
+    "tailwindcss-primeui": "^0.6.1",
     "tslib": "^2.8.1",
     "vue": "^3.5.13",
     "vue-router": "^4.5.0",

+ 92 - 0
scripts/UpdateScript/app.mjs

@@ -43,7 +43,11 @@ export async function viewVersion(type) {
         data = await axiosInstance.get('/version/' + type);
     }
     catch (e) {
+<<<<<<< HEAD
       logError('Failed to load version info', e);
+=======
+      console.error('Failed to load version info', e);
+>>>>>>> 7c11b917b224c1bfd3d86d6cc15ea83fede0098f
       return;
     }
     const table = new Table({
@@ -76,9 +80,15 @@ export async function getVersion(action, type) {
           status: 1,
           config: "{}",
         });
+<<<<<<< HEAD
         consola.success('Add version success');
       } catch (e) {
         logError('Failed to add version', e);
+=======
+        console.log('Add version success');
+      } catch (e) {
+        console.error('Failed to add version', e);
+>>>>>>> 7c11b917b224c1bfd3d86d6cc15ea83fede0098f
       }
       break;
     }
@@ -90,9 +100,15 @@ export async function getVersion(action, type) {
         return;
       try {
         await axiosInstance.delete('/version/' + versionId);
+<<<<<<< HEAD
         consola.success('删除版本成功');
       } catch (e) {
         logError('删除版本失败', e);
+=======
+        console.log('删除版本成功');
+      } catch (e) {
+        console.error('删除版本失败', e);
+>>>>>>> 7c11b917b224c1bfd3d86d6cc15ea83fede0098f
       }
       break;
     }
@@ -120,9 +136,15 @@ export async function getVersion(action, type) {
         await axiosInstance.put('/version/' + versionId, {
           status: state
         });
+<<<<<<< HEAD
         consola.success('设置状态成功');
       } catch (e) {
         logError('设置状态失败', e);
+=======
+        console.log('设置状态成功');
+      } catch (e) {
+        console.error('设置状态失败', e);
+>>>>>>> 7c11b917b224c1bfd3d86d6cc15ea83fede0098f
       }
       break;
     }
@@ -133,9 +155,15 @@ export async function getVersion(action, type) {
         await axiosInstance.put('/version/' + versionId, {
           config: config,
         });
+<<<<<<< HEAD
         consola.success('设置配置成功');
       } catch (e) {
         logError('设置配置失败', e);
+=======
+        console.log('设置配置成功');
+      } catch (e) {
+        console.error('设置配置失败', e);
+>>>>>>> 7c11b917b224c1bfd3d86d6cc15ea83fede0098f
       }
       break;
     }
@@ -146,9 +174,15 @@ export async function getVersion(action, type) {
 
       try {
         await axiosInstance.post('/update/active/app', { versionId, updateId, isNext });
+<<<<<<< HEAD
         consola.success('成功');
       } catch (e) {
         logError('失败', e);
+=======
+        console.log('成功');
+      } catch (e) {
+        console.error('失败', e);
+>>>>>>> 7c11b917b224c1bfd3d86d6cc15ea83fede0098f
       }
       break;
     }
@@ -158,14 +192,24 @@ export async function getVersion(action, type) {
 
       try {
         await axiosInstance.post('/update/active/web', { versionId, updateId });
+<<<<<<< HEAD
         consola.success('成功');
       } catch (e) {
         logError('失败', e);
+=======
+        console.log('成功');
+      } catch (e) {
+        console.error('失败', e);
+>>>>>>> 7c11b917b224c1bfd3d86d6cc15ea83fede0098f
       }
       break;
     }
     default:
+<<<<<<< HEAD
       logError('未知参数', action);
+=======
+      console.error('未知参数', action);
+>>>>>>> 7c11b917b224c1bfd3d86d6cc15ea83fede0098f
       break;
   }
 }
@@ -178,7 +222,11 @@ export async function selectVersion(defaultVersionId = null) {
     appId: config.appId 
   }))).data;
   if (data.length === 0) {
+<<<<<<< HEAD
     logError('没有版本,请在管理后台创建 appId ' + config.appId + ' 的版本');
+=======
+    console.error('没有版本');
+>>>>>>> 7c11b917b224c1bfd3d86d6cc15ea83fede0098f
     return;
   }
   const versionId = (await select({
@@ -196,7 +244,11 @@ export async function selectUpdate() {
   const { versionId } = await selectVersion();
   const data = (await axiosInstance.get('/version/update?search=' + JSON.stringify({ versionId }))).data;
   if (data.length === 0) {
+<<<<<<< HEAD
     logError('没有更新');
+=======
+    console.error('没有更新');
+>>>>>>> 7c11b917b224c1bfd3d86d6cc15ea83fede0098f
     return;
   }
   const resultId = (await select({
@@ -227,7 +279,11 @@ export async function postUpdate(type, options) {
       break;
     }
     default:
+<<<<<<< HEAD
       logError('Unknow type', type);
+=======
+      console.error('Unknow type', type);
+>>>>>>> 7c11b917b224c1bfd3d86d6cc15ea83fede0098f
       break;
   }
 }
@@ -252,11 +308,19 @@ export async function deprecateOrDeleteUpdate(updateId) {
   })) === 0;
 
   if (deprecate && currentUpdateInfo.type !== 1) {
+<<<<<<< HEAD
     consola.warn('只有Web更新允许弃用');
     return;
   }
   if (currentUpdateInfo.status === 0) {
     consola.warn(`当前状态 ${stateConstant[currentUpdateInfo.status]} 无法弃用`);
+=======
+    console.log('只有Web更新允许弃用');
+    return;
+  }
+  if (currentUpdateInfo.status === 0) {
+    console.log(`当前状态 ${stateConstant[currentUpdateInfo.status]} 无法弃用`);
+>>>>>>> 7c11b917b224c1bfd3d86d6cc15ea83fede0098f
     return;
   }
 
@@ -270,9 +334,15 @@ export async function deprecateOrDeleteUpdate(updateId) {
 
   try {
     await axiosInstance.post(`/update/${deprecate ? 'deprecate' : 'delete'}`, { updateId });
+<<<<<<< HEAD
     consola.success(`${deprecate ? '弃用' : '删除'} 成功`);
   } catch (e) {
     logError(`操作失败`, e);
+=======
+    console.log(`${deprecate ? '弃用' : '删除'} 成功`);
+  } catch (e) {
+    console.error(`操作失败`, e);
+>>>>>>> 7c11b917b224c1bfd3d86d6cc15ea83fede0098f
   }
 }
 export async function viewUpdate(updateId) {
@@ -283,12 +353,20 @@ export async function viewUpdate(updateId) {
   try {
     currentUpdateInfo = (await axiosInstance.get('/update/' + updateId)).data;
   } catch (e) {
+<<<<<<< HEAD
     logError('加载更新信息失败', updateId);
+=======
+    console.error('加载更新信息失败', updateId);
+>>>>>>> 7c11b917b224c1bfd3d86d6cc15ea83fede0098f
   }
   try {
     currentVersionInfo = (await axiosInstance.get('/version/' + currentUpdateInfo.versionId)).data;
   } catch (e) {
+<<<<<<< HEAD
     logError('加载版本信息失败', currentUpdateInfo.versionId);
+=======
+    console.error('加载版本信息失败', currentUpdateInfo.versionId);
+>>>>>>> 7c11b917b224c1bfd3d86d6cc15ea83fede0098f
   }
 
   const table = new Table({
@@ -351,7 +429,11 @@ export async function getUpdate(action, type, all, options) {
 
     if (type !== 'all' && all !== 'all' && data.data.length > 10) {
       data.data = data.data.slice(0, 10);
+<<<<<<< HEAD
       consola.info('filter!', all);
+=======
+      console.log('filter!', all);
+>>>>>>> 7c11b917b224c1bfd3d86d6cc15ea83fede0098f
     }
 
     data.data.forEach((d) => {
@@ -380,7 +462,11 @@ export async function getUpdate(action, type, all, options) {
         break;
       }
       default: 
+<<<<<<< HEAD
         logError('Unknow action', action);
+=======
+        console.error('Unknow action', action);
+>>>>>>> 7c11b917b224c1bfd3d86d6cc15ea83fede0098f
         break;
     }
   }
@@ -388,8 +474,14 @@ export async function getUpdate(action, type, all, options) {
 export async function testVersion(version) {
   try {
     const data = await axiosInstance.get('/update-get-info?version=' + version);
+<<<<<<< HEAD
     consola.info('版本信息', data.data);
   } catch(e) {
     logError('获取失败', e);
+=======
+    console.log('版本信息', data.data);
+  } catch(e) {
+    console.log('获取失败', e);
+>>>>>>> 7c11b917b224c1bfd3d86d6cc15ea83fede0098f
   }
 }

+ 53 - 0
scripts/UpdateScript/auth.mjs

@@ -28,7 +28,10 @@ export const constant = {
 
 let currentData = {
   token: '',
+<<<<<<< HEAD
   refreshToken: '',
+=======
+>>>>>>> 7c11b917b224c1bfd3d86d6cc15ea83fede0098f
   identifier: '',
 };
 
@@ -41,10 +44,13 @@ function getErrorMessage(e) {
   return e instanceof Error ? e.message : (typeof e === 'object' ? e : '' + e);
 }
 
+<<<<<<< HEAD
 async function saveCurrentData() {
   await writeFile(constant.TokenSave, JSON.stringify(currentData));
 }
 
+=======
+>>>>>>> 7c11b917b224c1bfd3d86d6cc15ea83fede0098f
 // Axios 实例
 // ========================================
 
@@ -71,6 +77,7 @@ axiosInstance.interceptors.request.use((value) => {
   return value;
 });
 
+<<<<<<< HEAD
 let refreshPromise = null;
 
 async function tryRefreshToken() {
@@ -133,13 +140,22 @@ axiosInstance.interceptors.response.use(null, async (error) => {
     }
   }
   return Promise.reject(error);
+=======
+axiosInstance.interceptors.response.use((value) => {
+  if (value.data.success) return value.data;
+  else return Promise.reject(value.data);
+>>>>>>> 7c11b917b224c1bfd3d86d6cc15ea83fede0098f
 });
 
 // 认证初始化
 // ========================================
 
 /**
+<<<<<<< HEAD
  * 初始化认证模块,从本地加载 token,并检查登录状态
+=======
+ * 初始化认证模块,从本地加载 token
+>>>>>>> 7c11b917b224c1bfd3d86d6cc15ea83fede0098f
  * @returns {Promise<void>}
  */
 export async function initAuth() {
@@ -153,6 +169,7 @@ export async function initAuth() {
     // 无 token 文件时使用默认值
   }
   initIdentifier();
+<<<<<<< HEAD
 
   if (currentData.token) {
     try {
@@ -180,6 +197,8 @@ export async function initAuth() {
 
   consola.warn('未登录,进入登录界面');
   await interactiveLogin();
+=======
+>>>>>>> 7c11b917b224c1bfd3d86d6cc15ea83fede0098f
 }
 
 /**
@@ -193,6 +212,7 @@ export function getCurrentData() {
 // ========================================
 
 /**
+<<<<<<< HEAD
  * 执行登录请求并保存 token
  * @param {string} user 用户名
  * @param {string} pass 密码
@@ -231,14 +251,22 @@ async function interactiveLogin() {
 }
 
 /**
+=======
+>>>>>>> 7c11b917b224c1bfd3d86d6cc15ea83fede0098f
  * 检查登录状态
  */
 export async function checkLogged() {
   try {
     await axiosInstance.get('/auth');
+<<<<<<< HEAD
     consola.success('已登录');
   } catch (e) {
     logError('获取状态失败:', getErrorMessage(e));
+=======
+    console.log('已登录');
+  } catch (e) {
+    console.error('获取状态失败:', getErrorMessage(e));
+>>>>>>> 7c11b917b224c1bfd3d86d6cc15ea83fede0098f
   }
 }
 
@@ -248,12 +276,28 @@ export async function checkLogged() {
  */
 export async function login(user) {
   try {
+<<<<<<< HEAD
     const username = user || await input({ message: '输入用户名' });
     const pass = await password({ message: '输入密码' });
     await doLogin(username, pass);
     consola.success('登录成功');
   } catch (e) {
     logError('登录失败', getErrorMessage(e));
+=======
+    const pass = await password({ message: '输入密码' });
+    const res = await axiosInstance.post('/auth?rember=true', {
+      method: 'key',
+      key: `${user}@${md5(pass)}`,
+    });
+    currentData.token = {
+      authName: res.data.authName,
+      authKey: res.data.authKey,
+    };
+    await writeFile(constant.TokenSave, JSON.stringify(currentData));
+    console.log('登录成功');
+  } catch (e) {
+    console.error('登录失败', getErrorMessage(e));
+>>>>>>> 7c11b917b224c1bfd3d86d6cc15ea83fede0098f
   }
 }
 
@@ -262,6 +306,7 @@ export async function login(user) {
  */
 export async function logout() {
   currentData.token = '';
+<<<<<<< HEAD
   currentData.refreshToken = '';
   await saveCurrentData();
   try {
@@ -269,5 +314,13 @@ export async function logout() {
     consola.success('退出登录成功');
   } catch (e) {
     logError('退出登录失败', getErrorMessage(e));
+=======
+  await writeFile(constant.TokenSave, JSON.stringify(currentData));
+  try {
+    await axiosInstance.delete('/auth');
+    console.log('退出登录成功');
+  } catch (e) {
+    console.error('退出登录失败', getErrorMessage(e));
+>>>>>>> 7c11b917b224c1bfd3d86d6cc15ea83fede0098f
   }
 }

+ 6 - 0
scripts/UpdateScript/index.mjs

@@ -47,6 +47,7 @@ function start() {
 
 initAuth().then(start);
 
+<<<<<<< HEAD
 process.on('unhandledRejection', (reason) => {
   let reasonStr = '';
   if (typeof reason === 'string') {
@@ -61,3 +62,8 @@ process.on('unhandledRejection', (reason) => {
   logError('UnhandledRejection:', reasonStr)
 })
 
+=======
+process.on('unhandledRejection', (reason, p) => {
+  console.error('Promise: ', p, 'Reason: ', reason)
+})
+>>>>>>> 7c11b917b224c1bfd3d86d6cc15ea83fede0098f

+ 1 - 0
server/api/ecms/article/[id].ts

@@ -11,6 +11,7 @@ export interface IArticle {
   desc: string;
   image: string,
   video?: string,
+  document_file?: string,
   images: string;
   seotitle: string;
   keywords: string;

+ 32 - 14
server/api/ecms/article/byChannelName.ts

@@ -11,28 +11,46 @@ export default defineEventHandler<EventHandlerRequest, Promise<IResponse<ICommon
     const query = getQuery(event);
     const page = Number(query.page as string) || 1;
     const pageSize = Number(query.pageSize as string) || 10;
+    const onlySelf = query.onlySelf === 'true';
+    const bannedChannels = (query.bannedChannels as string || '').split(',');
+
     const channelName = query.channelName as string;
     if (!channelName)
       return createErrorResponse('分类名称不能为空');
+
+    const channelNames = channelName.split(',');
     // 1. 从pr_cms_channel表中通过name查询channel_id
-    const channel = await DB.table('pr_cms_channel')
-      .where('name', channelName)
-      .where('status', 'normal')
-      .select('id')
-      .first();
+
+    const channelIds : number[] = [];
+    for (const channelName of channelNames) {
+      const channel = await DB.table('pr_cms_channel')
+        .where('name', channelName)
+        .where('status', 'normal')
+        .select('id')
+        .first();
+      if (channel) {
+        channelIds.push(channel.id);
+        if (!onlySelf) {
+          const subChannels = (await DB.table('pr_cms_channel')
+            .where('parent_id', channel.id)
+            .where('status', 'normal')
+            .select('id', 'name')
+            .get()).map(item => item.id);
+          if (bannedChannels.length > 0) {
+            channelIds.push(...subChannels.filter(item => !bannedChannels.includes(item.name)).map(item => item.id));
+          } else
+            channelIds.push(...subChannels.map(item => item.id));
+        }
+      }
+    }
+    
     // 如果没有找到对应的频道,返回空数组
-    if (!channel) 
+    if (channelIds.length === 0) 
       return createSuccessResponse(new CommonPageResult<IArticle>(undefined, [], page, pageSize, 0));
-        
-    const subChannelIds = (await DB.table('pr_cms_channel')
-      .where('parent_id', channel.id)
-      .where('status', 'normal')
-      .select('id')
-      .get()).map(item => item.id);
 
     // 2. 从pr_cms_archives表中通过channel_id查询文章
     const articles = await DB.table('pr_cms_archives')
-            .whereIn('channel_id', [channel.id, ...subChannelIds])
+            .whereIn('channel_id', channelIds)
             .where('status', 'normal')
             .whereNull('deletetime')
             .orderBy('weigh', 'desc')
@@ -44,7 +62,7 @@ export default defineEventHandler<EventHandlerRequest, Promise<IResponse<ICommon
       return createErrorResponse('文章不存在');
 
     return createSuccessResponse({
-      channel_id: channel.id,
+      channel_id: channelIds[0],
       ...articles
     });
   } catch (error) {

+ 7 - 3
server/api/ecms/channel/[id].ts

@@ -19,6 +19,8 @@ export default defineEventHandler<EventHandlerRequest, Promise<IResponse<IChanne
 }>>>(async (event) => {
   try {
     const id = event.context.params?.id;
+    const query = getQuery(event);
+    const onlyNavVisible = query.onlyNavVisible === 'true';
     if (!id)
       return createErrorResponse('分类ID不能为空');
     
@@ -29,12 +31,14 @@ export default defineEventHandler<EventHandlerRequest, Promise<IResponse<IChanne
     if (!category) 
       return createErrorResponse('分类不存在');
 
-    const childs = await DB.table('pr_cms_channel')
+    const query2 = DB.table('pr_cms_channel')
       .where('status', 'normal')
       .where('parent_id', id)
       .orderBy('weigh', 'desc')
-      .orderBy('id', 'desc')
-      .get();
+      .orderBy('id', 'desc');
+    if (onlyNavVisible)
+      query2.where('isnav', 1);
+    const childs = await query2.get();
 
     const parents : IChannel[] = [];
     let parentId = category.parent_id;

+ 3 - 0
server/api/ecms/channel/nav.ts

@@ -8,6 +8,9 @@ export default defineEventHandler<EventHandlerRequest, Promise<IResponse<IChanne
     return createSuccessResponse(await DB.table('pr_cms_channel')
             .where('status', 'normal')
             .where('isnav', 1)
+            .where('parent_id', 0)
+            .orderBy('weigh', 'desc')
+            .orderBy('id', 'asc')
             .select('*')
             .get());
   } catch (error) {

+ 2 - 0
server/api/ecms/channel/sitemap.ts

@@ -16,6 +16,7 @@ export default defineEventHandler<EventHandlerRequest, Promise<IResponse<(IChann
     const navs = await DB.table('pr_cms_channel')
       .where('status', 'normal')
       .where('isnav', 1)
+      .orderBy('weigh', 'desc')
       .select('*')
       .get()
 
@@ -23,6 +24,7 @@ export default defineEventHandler<EventHandlerRequest, Promise<IResponse<(IChann
       const childs = await DB.table('pr_cms_channel')
         .where('status', 'normal')
         .where('parent_id', nav.id)
+        .orderBy('weigh', 'desc')
         .select('*')
         .get();
       result.push({

+ 2 - 1
src/App.vue

@@ -12,7 +12,8 @@
     <main>
       <NuxtPage />
     </main>
-    <Footer />
+    <Footer>
+    </Footer>
   </a-config-provider>
 </template>
 

+ 2 - 2
src/api/RequestModules.ts

@@ -177,9 +177,9 @@ export class AppServerRequestModule<T extends DataModel> extends RequestCoreInst
     super(WebFetchImplementer);
     this.config.baseUrl = ApiCofig.serverProd;
     this.config.errCodes = []; //
-    this.config.requestInceptor = requestInceptor;
+    this.config.requestInterceptor = requestInceptor;
     this.config.responseDataHandler = responseDataHandler;
-    this.config.responseErrReoprtInceptor = responseErrReoprtInceptor;
+    this.config.responseErrorReportInterceptor = responseErrReoprtInceptor;
     this.config.reportError = reportError;
   }
 }

BIN
src/assets/images/banner/1.jpg


BIN
src/assets/images/banner/2.jpg


BIN
src/assets/images/bars/1.jpg


BIN
src/assets/images/bars/2.jpg


BIN
src/assets/images/bars/3.jpg


BIN
src/assets/images/bars/4.jpg


BIN
src/assets/images/bars/5.jpg


BIN
src/assets/images/bars/6.jpg


BIN
src/assets/images/bars/7.jpg


BIN
src/assets/images/bars/8.jpg


BIN
src/assets/images/index-title.png


BIN
src/assets/images/juzhen.jpg


Разница между файлами не показана из-за своего большого размера
+ 9 - 0
src/assets/images/link-infos/icons/dy.svg


Разница между файлами не показана из-за своего большого размера
+ 13 - 0
src/assets/images/link-infos/icons/gzhh.svg


Разница между файлами не показана из-за своего большого размера
+ 12 - 0
src/assets/images/link-infos/icons/sph.svg


Разница между файлами не показана из-за своего большого размера
+ 14 - 0
src/assets/images/link-infos/icons/xhs.svg


BIN
src/assets/images/link-infos/qrcode/dy.jpg


BIN
src/assets/images/link-infos/qrcode/gzhh.jpg


BIN
src/assets/images/link-infos/qrcode/mn.png


BIN
src/assets/images/link-infos/qrcode/sph.jpg


BIN
src/assets/images/link-infos/qrcode/xhs.jpg


BIN
src/assets/images/qr1/activity.png


BIN
src/assets/images/qr1/volunteer.png


BIN
src/assets/images/xuexi.png


+ 117 - 9
src/assets/scss/main.scss

@@ -75,9 +75,9 @@ header {
   .inner {
     display: flex;
     flex-direction: row;
-    justify-content: space-between;
+    justify-content: center;
     align-items: center;
-    max-width: var(--container-width);
+    //max-width: var(--container-width);
     margin: 0 auto;
   }
   .logo {
@@ -115,11 +115,36 @@ header {
       margin-right: 10px;
     }
   }
+  .action-buttons {
+    position: relative;
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    justify-content: center;
+    padding-left: 10px;
+  }
+
   .mobile-menu-toggle {
     display: none;
     width: 60px;
     height: 60px;
   }
+  .extra-menu-toggle {
+    width: 60px;
+    height: 60px;
+    display: flex;
+    background-color: transparent;
+    border: none;
+    padding: 0;
+    margin: 0;
+    cursor: pointer;
+    .iconify {
+      color: var(--color-text);
+    }
+    &:hover {
+      background-color: var(--color-box);
+    }
+  }
 }
 
 button {
@@ -190,6 +215,7 @@ nav.main-nav {
     display: flex;
     justify-content: center;
     align-items: center;
+    flex-wrap: wrap;
     margin-bottom: 0;
   }
   .nav-item {
@@ -200,6 +226,9 @@ nav.main-nav {
       color: var(--color-primary);
     }
   }
+  .mobile-close-button {
+    display: none;
+  }
   a {
     display: block;
     color: var(--color-text);
@@ -214,6 +243,39 @@ nav.main-nav {
   }
 }
 
+.extra-nav-list {
+  position: absolute;
+  display: flex;
+  flex-direction: column;
+  margin-bottom: 0;
+  top: 100%;
+  left: -50px;
+  background-color: var(--color-box);
+  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
+  padding: 10px;
+  z-index: 100;
+
+  .nav-item {
+    position: relative;
+
+    &.active a {
+      font-weight: bold;
+      color: var(--color-primary);
+    }
+  }
+  a {
+    display: block;
+    color: var(--color-text);
+    padding: 12px 10px;
+    font-weight: 500;
+    white-space: nowrap;
+    transition: background-color 0.3s;
+
+    &:hover, &:active {
+      color: var(--color-primary);
+    }
+  }
+}
 
 .search-bar {
   position: relative;
@@ -255,6 +317,7 @@ nav.main-nav {
   background-color: var(--color-box);
   border-radius: 10px;
   padding: 26px;
+  overflow: hidden;
 
   &.fill {
     height: 100%;
@@ -266,7 +329,7 @@ nav.main-nav {
 }
 .main-header-image {
   width: 100%;
-  height: 400px;
+  height: 400px!important;
   object-fit: cover;
 }
 
@@ -466,6 +529,13 @@ nav.main-nav {
     }
   }
 
+  .publishtime {
+    color: #999;
+    font-size: 14px;
+    flex-shrink: 0;
+    white-space: nowrap;
+  }
+
   .no-image,
   img {
     width: 100px;
@@ -524,7 +594,7 @@ nav.main-nav {
 }
 .featured-grid {
   display: grid;
-  grid-template-columns: repeat(3, 1fr);
+  grid-template-columns: repeat(4, 1fr);
   gap: 20px;
 }
 .featured-card {
@@ -660,7 +730,7 @@ footer {
 
   select {
     color: #666;
-    padding: 8px 45px 8px 15px;
+    padding: 8px 15px 8px 15px;
     border: 1px solid rgba(221, 221, 221, 0.6);
     border-radius: 6px;
     background-color: rgba(255, 255, 255, 0.25);
@@ -897,13 +967,51 @@ footer {
       padding: 0 30px;
     }
     .logo {
+      flex-direction: column;
+      align-items: flex-start;
       font-size: 20px;
+      min-width: 100px;
+      margin-right: 20px;
+
+      span {
+        margin-right: 0;
+      }
+
+      .col {
+        padding-left: 0;
+
+        span {
+          font-size: 12px;
+          margin-bottom: 0;
+        }
+
+        &::before {
+          display: none;
+        }
+      }
     }
   }
-  nav.main-nav .nav-item {
-    padding: 0;
-    a {
-      padding: 10px 5px;
+  nav.main-nav {
+
+    .mobile-close-button {
+      display: block;
+      position: absolute;
+      bottom: 0;
+      right: 0;
+      width: 40px;
+      height: 40px;
+      z-index: 100;
+    }
+    .nav-list {
+      position: relative;
+      padding: 0;
+
+      .nav-item {
+        padding: 0;
+        a {
+          padding: 10px 5px;
+        }
+      }
     }
   }
 }

+ 71 - 31
src/components/Footer.vue

@@ -2,37 +2,57 @@
   <!-- 底部 -->
   <footer>
     <div class="container">
-      <div class="footer-select">
-        <select name="related-links" id="related-links" @change="handleLinkChange">
-          <option value="">相关网站链接</option>
-          <option value="http://www.ncha.gov.cn/index.html">国家文物局</option>
-          <option value="https://wlt.fujian.gov.cn/">福建省文化和旅游厅</option>
-          <option value="https://www.xm.gov.cn/">厦门市人民政府</option>
-          <option value="https://www.ihchina.cn/">中国非物质文化遗产网</option>
-        </select>
+      <div class="row d-flex flex-row justify-content-center">
+        <div class="col col-12">
+          <slot />
+        </div>
       </div>
-      
-      <div class="footer-links">
-        <template v-for="link in footerInfo.links" :key="link.url">
-          <template v-if="link.url.startsWith('http')">
-            <a :href="link.url" class="footer-link">
-              {{ link.name }}
-            </a>
-          </template>
-          <template v-else>
-            <router-link :to="link.url" class="footer-link">
-              {{ link.name }}
-            </router-link>
-          </template>
-        </template>
-      </div>
-      
-      <div class="footer-info">
-        <p>闽公网安备35020302036685号 | <a href="https://beian.miit.gov.cn/">闽ICP备2025115303号-1</a> | 
-          <span id="_ideConac"><a href="https://bszs.conac.cn/sitename?method=show&amp;id=43C18E2A812B1ABAE06310291AACF961" target="_blank"><img id="imgConac" vspace="0" hspace="0" border="0" src="https://dcs.conac.cn/image/blue_error.png" data-bd-imgshare-binded="1"></a></span>
-        </p>
-        <p>联系地址: {{ footerInfo.address }}</p>
-        <p>联系电话: {{ footerInfo.contactPhone }} | 工作电话: {{ footerInfo.workPhone }}</p>
+      <div class="row d-flex flex-row justify-content-center">
+        <div class="col col-sm-12 col-md-6 col-lg-6">
+
+          <div class="footer-select">
+            <select name="related-links-superior" id="related-links-superior" @change="handleLinkChange">
+              <option value="">上级主管部门</option>
+              <option value="http://www.ncha.gov.cn/index.html">国家文物局</option>
+              <option value="https://wlt.fujian.gov.cn/">福建省文化和旅游厅</option>
+              <option value="http://wlj.xm.gov.cn/">厦门市文化和旅游局</option>
+            </select>
+            <select name="related-links-municipal" id="related-links-municipal" @change="handleLinkChange">
+              <option value="">市政府部门网站</option>
+              <option value="https://www.xm.gov.cn/">厦门市人民政府</option>
+            </select>
+            <select name="related-links-units" id="related-links-units" @change="handleLinkChange">
+              <option value="">局属单位</option>
+              <option value="https://www.ihchina.cn/">中国非物质文化遗产网</option>
+              <option value="https://www.xmlib.net/">厦门市图书馆</option>
+              <option value="https://www.xmwhg.com.cn/">厦门市文化馆</option>
+              <option value="http://xmmuseum.com/">厦门市博物馆</option>
+            </select>
+          </div>
+          
+          <div class="footer-links">
+            <template v-for="link in footerInfo.links" :key="link.url">
+              <template v-if="link.url.startsWith('http')">
+                <a :href="link.url" class="footer-link">
+                  {{ link.name }}
+                </a>
+              </template>
+              <template v-else>
+                <router-link :to="link.url" class="footer-link">
+                  {{ link.name }}
+                </router-link>
+              </template>
+            </template>
+          </div>
+          
+          <div class="footer-info">
+            <p>闽公网安备35020302036685号 | <a href="https://beian.miit.gov.cn/">闽ICP备2025115303号-1</a> | 
+              <span id="_ideConac"><a href="https://bszs.conac.cn/sitename?method=show&amp;id=43C18E2A812B1ABAE06310291AACF961" target="_blank"><img id="imgConac" vspace="0" hspace="0" border="0" src="https://dcs.conac.cn/image/blue_error.png" data-bd-imgshare-binded="1"></a></span>
+            </p>
+            <p>联系地址: {{ footerInfo.address }}</p>
+            <p>联系电话: {{ footerInfo.contactPhone }} | 工作电话: {{ footerInfo.workPhone }}</p>
+          </div>
+        </div>
       </div>
     </div>
   </footer>
@@ -63,4 +83,24 @@ function handleLinkChange(event: Event) {
   }
 }
 
-</script>
+</script>
+
+<style scoped lang="scss">  
+.footer-select {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  gap: 10px;
+
+  select {
+    width: 33%;;
+  }
+}
+.media-matrix {
+  width: 100%;
+  height: 100%;
+  max-width: 250px;
+  object-fit: cover;
+}
+</style>

+ 171 - 0
src/components/LinkInfos.vue

@@ -0,0 +1,171 @@
+<template>
+  <div :class="['link-infos', showType]">
+    <div v-for="item in linkInfos" :key="item.name">
+      <img 
+        v-if="showType === 'float'"
+        :src="item.icon" 
+        :alt="item.name" 
+        :class="{ large: item.large }" 
+      />
+      <img 
+        v-else-if="showType === 'list'"
+        :src="item.url" 
+        :alt="item.name" 
+        :class="{ large: item.large }" 
+      />
+      <span>{{ item.name }}</span>
+      <div v-if="showType === 'float'" class="popup">
+        <img :src="item.url" :alt="item.name" />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import WechatIcon from '@/assets/images/link-infos/icons/gzhh.svg';
+import VideoIcon from '@/assets/images/link-infos/icons/sph.svg';
+import DouyinIcon from '@/assets/images/link-infos/icons/dy.svg';
+import XhsIcon from '@/assets/images/link-infos/icons/xhs.svg';
+import WechatUrl from '@/assets/images/link-infos/qrcode/gzhh.jpg';
+import VideoUrl from '@/assets/images/link-infos/qrcode/sph.jpg';
+import DouyinUrl from '@/assets/images/link-infos/qrcode/dy.jpg';
+import XhsUrl from '@/assets/images/link-infos/qrcode/xhs.jpg';
+import Mn from '@/assets/images/link-infos/qrcode/mn.png';
+
+defineProps({
+  showType: {
+    type: String,
+    default: 'float',
+    validator: (value: string) => {
+      return ['float', 'list'].includes(value);
+    },
+  },
+});
+const linkInfos = ref([
+  {
+    name: '公众号',
+    icon: WechatIcon,
+    url: WechatUrl,
+  },
+  {
+    name: '视频号',
+    icon: VideoIcon,
+    url: VideoUrl,
+  },
+  {
+    name: '抖音',
+    icon: DouyinIcon,
+    url: DouyinUrl,
+  },
+  {
+    name: '小红书',
+    icon: XhsIcon,
+    url: XhsUrl,
+    large: true,
+  },
+  {
+    name: '闽南文化小程序',
+    icon: Mn,
+    url: Mn,
+    large: true,
+  },
+]);
+</script>
+
+<style lang="scss">
+.link-infos {
+  position: fixed;
+  background-color: #484848;
+  right: 40px;
+  bottom: 30px;
+  width: 60px;
+  z-index: 1000;
+  display: flex;
+  flex-direction: column;
+  align-items: stretch;
+  justify-content: center;
+  gap: 10px;
+  padding: 10px 0;
+
+  &.list {
+    position: relative;
+    background-color: transparent;
+    flex-direction: row;
+    flex-wrap: wrap;
+    align-items: center;
+    justify-content: center;
+    gap: 56px;
+    padding: 10px 0 46px 0;
+    width: auto;
+    left: unset;
+    right: unset;
+    bottom: unset;
+
+    > div {
+      gap: 10px;
+      font-size: 20px;
+      color: #222;
+
+      &:hover {
+        background-color: transparent;
+      }
+
+      > img {
+        width: 170px;
+        height: 170px;
+        object-fit: contain;
+      }
+    }
+  }
+
+  > div {
+    position: relative;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    font-size: 12px;
+    padding: 5px 0;
+    gap: 5px;
+    color: #999;
+
+    > img {
+      width: 20px;
+      height: 20px;
+      object-fit: contain;
+
+      &.large {
+        width: 100%;
+      }
+    }
+
+    &:hover {
+      background-color: #616161;
+      
+      .popup {
+        display: block;
+      }
+    }
+
+    .popup {
+      img {
+        width: 100%;
+        height: 100%;
+        object-fit: contain;
+      }
+
+      display: none;
+      position: absolute;
+      bottom: 0;
+      left: 0;
+      transform: translateX(-100%);
+      width: 160px;
+      height: 160px;
+      padding: 10px;
+      background-color: #fff;
+      border: 1px solid #616161;
+      z-index: 1000;
+    }
+  }
+}
+</style>

+ 57 - 9
src/components/NavBar.vue

@@ -11,16 +11,36 @@
       </div>
       <!-- 导航 -->
       <nav class="main-nav" :class="{ show: isMenuOpen }">
-        <div class="container">
-          <ul class="nav-list">
-            <li v-for="item in navItems.content.value" :key="item.url" class="nav-item" :class="{ active: isActive(item.url) }">
-              <ChannelLink :item="item" />
-            </li>
-          </ul>
-        </div>
+        <ul class="nav-list">
+          <li v-for="item in navItemsFinal" :key="item.url" class="nav-item" :class="{ active: isActive(item.url) }">
+            <ChannelLink :item="item" />
+          </li>
+        </ul>
+        <button class="mobile-close-button" @click="isMenuOpen = false"><Icon name="material-symbols:close" /></button>
       </nav>
-      <div class="d-flex flex-row align-items-center">
-        <button class="mobile-menu-toggle" @click="toggleMenu"><Icon name="material-symbols:menu" /></button>
+      <div class="action-buttons">
+        <button 
+          v-if="navItemsOverflow"
+          class="extra-menu-toggle" 
+          @click="toggleExtraMenu">
+          <Icon name="material-symbols-light:more-horiz" size="34" />
+        </button>
+        <ul class="extra-nav-list" v-if="isExtraMenuOpen">
+          <li 
+            v-for="item in navItemsExtra" :key="item.url"
+            class="nav-item" 
+            :class="{ active: isActive(item.url) }"
+            @click="toggleExtraMenu"
+          >
+            <ChannelLink :item="item" />
+          </li>
+        </ul>
+
+        <button
+          class="mobile-menu-toggle" 
+          @click="toggleMenu">
+          <Icon name="material-symbols:menu" size="34" />
+        </button>
         <div class="search-bar">
           <button @click="toggleSearch"><Icon name="material-symbols:search-rounded" /></button>
           <input type="text" placeholder="搜索..." v-model="keyword" @blur="hideSearch" @keydown.enter="goSearch" :class="{ show: isSearchOpen }">
@@ -36,6 +56,7 @@ import { useRoute } from 'vue-router';
 import { solveChannelData } from '~/composeable/InternalData';
 import { useSSrSimpleDataLoader } from '~/composeable/SimpleDataLoader';
 import ChannelLink from './link/ChannelLink.vue';
+import { useWindowSizeListener } from '~/composeable/WindowSizeListener';
 
 const route = useRoute();
 
@@ -44,7 +65,25 @@ const navItems = await useSSrSimpleDataLoader('navItems', async () => {
   const items = solveChannelData(data);
   return items;
 });
+const navItemsFinal = computed(() => {
+  if (navItemsOverflow.value)
+    return navItems.content.value?.slice(0, 7);
+  return navItems.content.value;
+});
+const navItemsExtra = computed(() => {
+  if (navItemsOverflow.value)
+    return navItems.content.value?.slice(7);
+  return [];
+});
+const { windowWidth } = useWindowSizeListener();
+const navItemsOverflow = computed(() => 
+  import.meta.client && (windowWidth.value > 992 && windowWidth.value < 1400) 
+  && (navItems.content.value?.length || 0) > 8
+);
+
+
 const isMenuOpen = ref(false);
+const isExtraMenuOpen = ref(false);
 const isSearchOpen = ref(false);
 const keyword = ref('');
 
@@ -59,6 +98,9 @@ function toggleMenu() {
 function toggleSearch() {
   isSearchOpen.value = !isSearchOpen.value;
 }
+function toggleExtraMenu() {
+  isExtraMenuOpen.value = !isExtraMenuOpen.value;
+}
 function hideSearch() {
   isSearchOpen.value = false;
 }
@@ -67,4 +109,10 @@ function goSearch() {
     window.location.href = `/search/?keyword=${encodeURIComponent(keyword.value.trim())}`;
   }
 }
+
+watch(route, () => {
+  isMenuOpen.value = false;
+  isExtraMenuOpen.value = false;
+  isSearchOpen.value = false;
+});
 </script>

+ 1 - 1
src/components/Sidebar.vue

@@ -47,7 +47,7 @@ const props = defineProps({
   },
   showEmpty: {
     type: Boolean,
-    default: true,
+    default: false,
   },
   backUpLevelLink: {
     type: String,

+ 30 - 23
src/components/content/CommonListPage.vue

@@ -2,7 +2,7 @@
   <!-- 通用列表 -->
   <PageContainer :title="title">
     <template #carousel>
-      <Carousel v-if="false" v-bind="carouselConfig" class="main-header-image carousel-light">
+      <Carousel v-if="showCarousel" v-bind="carouselConfig" class="main-header-image carousel-light">
         <Slide 
           v-for="(item, key) in carouselData.content.value"
           :key="key"
@@ -65,28 +65,6 @@ const carouselConfig : (typeof Carousel['props']) = {
 
 const router = useRouter();
 const route = useRoute();
-
-const carouselData = await useSSrSimpleDataLoader('carousel', async () => {
-  const res = await $fetch(`/api/ecms/carousel`);
-  if (!res.status)
-    throw new Error(res.message);
-  return res.data;
-});
-const channelId = parseInt(route.query.parent_channel_id as string);
-const channelData = await useSSrSimpleDataLoader('channel' + channelId, async () => {
-  const res = await $fetch(`/api/ecms/channel/${channelId}`);
-  if (!res.status)
-    throw new Error(res.message);
-  const data =  res.data as IChannel & {
-    childs: IChannel[];
-    parents: IChannel[];
-  };
-  return {
-    ...data,
-    childs: solveChannelData(data.childs),
-  };
-});
-
 const props = defineProps({	
   title: {
     type: String,
@@ -103,6 +81,14 @@ const props = defineProps({
     type: Object as PropType<DropDownNames[]>,
     default: null,
   },
+  carouselName: {
+    type: String,
+    default: 'banner',
+  },
+  showCarousel: {
+    type: Boolean,
+    default: false,
+  },
   showSearch: {
     type: Boolean,
     default: true,
@@ -179,6 +165,27 @@ const props = defineProps({
     default: 0,
   }
 })
+
+const carouselData = await useSSrSimpleDataLoader('carousel-' + (props.carouselName || 'default'), async () => {
+  const res = await $fetch(`/api/ecms/carousel?name=${props.carouselName}`);
+  if (!res.status)
+    throw new Error(res.message);
+  return res.data;
+});
+const channelId = parseInt(route.query.parent_channel_id as string);
+const channelData = await useSSrSimpleDataLoader('channel' + channelId, async () => {
+  const res = await $fetch(`/api/ecms/channel/${channelId}`);
+  if (!res.status)
+    throw new Error(res.message);
+  const data =  res.data as IChannel & {
+    childs: IChannel[];
+    parents: IChannel[];
+  };
+  return {
+    ...data,
+    childs: solveChannelData(data.childs),
+  };
+});
 </script>
 
 <style lang="scss">

+ 15 - 0
src/components/link/PageLink.vue

@@ -0,0 +1,15 @@
+<template>
+  <a v-if="article.outlink" :href="article.outlink" target="_blank">{{ article.title }}</a>
+  <router-link v-else :to="'/page/' + article.id" class="title">{{ article.title }}</router-link>
+</template>
+
+<script setup lang="ts">
+import type { IArticle } from '~~/server/api/ecms/article/[id]';
+
+const props = defineProps({
+  article: {
+    type: Object as PropType<IArticle>,
+    required: true,
+  }
+});
+</script>

+ 22 - 0
src/composeable/WindowSizeListener.ts

@@ -0,0 +1,22 @@
+export function useWindowSizeListener() {
+  const windowWidth = ref(import.meta.client ? window.innerWidth : 0);
+  const windowHeight = ref(import.meta.client ? window.innerHeight : 0);
+
+  function updateWindowSize() {
+    windowWidth.value = window.innerWidth;
+    windowHeight.value = window.innerHeight;
+  }
+
+  onMounted(() => {
+    window.addEventListener('resize', updateWindowSize);
+  });
+  onUnmounted(() => {
+    window.removeEventListener('resize', updateWindowSize);
+  });
+
+
+  return {
+    windowWidth,
+    windowHeight,
+  }
+}

+ 23 - 3
src/pages/about.vue

@@ -9,11 +9,21 @@
     </Head>
 
     <!-- 轮播图 -->
-    <img class="main-header-image" src="https://xmswhycbhzx.cn/uploads/20260106/c3fb211f95655f98a31d099f4782e6ca.jpg">
+    <img class="main-header-image" src="@/assets/images/banner/1.jpg">
 
     <!-- 主要内容 -->
     <div class="main-content">
       <div class="container">
+        <div class="section-title center large mt-5">
+          <h2 class="icon">单位概况</h2>
+        </div>
+        <div class="row">
+          <div class="col col-12">
+            <h3 class="text-center">{{ aboutData.content.value?.title }}</h3>
+            <div v-html="aboutData.content.value?.content"></div>
+          </div>
+        </div>
+        
         <div class="section-title center large mt-4">
           <h2 class="icon">联系方式</h2>
         </div>
@@ -26,7 +36,7 @@
                 电话: 0592-2085737
               </li>
               <li class="info-item">
-                <Icon name="material-symbols:email" />
+                <Icon name="material-symbols:attach-email" />
                 邮箱: hlspt@xm.gov.cn
               </li>
               <li class="info-item">
@@ -35,7 +45,7 @@
               </li>
               <li class="info-item">
                 <Icon name="material-symbols:schedule" />
-                办公时间: 周一至周五 8:00-17:00
+                办公时间: 8:30-12:00 14:00-17:30
               </li>
               <li class="info-item">
                 <Icon name="material-symbols:map" />
@@ -65,12 +75,22 @@
 </template>
 
 <script setup lang="ts">
+import { useSSrSimpleDataLoader } from '~/composeable/SimpleDataLoader';
 
 onMounted(() => {
   if (import.meta.client)
     initMap();
 });
 
+const aboutData = await useSSrSimpleDataLoader('articles111', async () => {
+  const res = await $fetch(`/api/ecms/article/${111}`, {
+    method: 'GET',
+  });
+  if (!res.status)
+    throw new Error(res.message);
+  return res.data;
+});
+
 // 初始化地图
 function initMap() {
   setTimeout(() => {

+ 3 - 3
src/pages/channel/[id].vue

@@ -49,8 +49,7 @@
               <img v-if="item.image" :src="item.image" />
               <div v-else-if="anyItemHasImage" class="no-image">暂无图片</div>
               <div class="desc">
-                <a v-if="item.outlink" :href="item.outlink" target="_blank">{{ item.title }}</a>
-                <router-link v-else :to="'/page/' + item.id" class="title">{{ item.title }}</router-link>
+                <PageLink :article="item" class="title" />
                 <p class="desc">{{ item.description }}</p>
               </div>
             </div>
@@ -78,6 +77,7 @@ import { DateUtils } from '@imengyu/imengyu-utils';
 import type { IChannel } from '~~/server/api/ecms/channel/[id]';
 import SimplePagination from '~/components/content/SimplePagination.vue';
 import { getNeedJumpToInternalPage, solveChannelData } from '~/composeable/InternalData';
+import PageLink from '~/components/link/PageLink.vue';
 
 const carouselConfig : (typeof Carousel['props']) = {
   itemsToShow: 1,
@@ -91,7 +91,7 @@ const router = useRouter();
 const channelId = parseInt(route.params.id as string);
 
 const channelData = await useSSrSimpleDataLoader('channel' + channelId, async () => {
-  const res = await $fetch(`/api/ecms/channel/${channelId}`);
+  const res = await $fetch(`/api/ecms/channel/${channelId}?onlyNavVisible=true`);
   if (!res.status)
     throw new Error(res.message);
   const data = res.data as IChannel & {

+ 6 - 12
src/pages/channel/laws.vue

@@ -11,7 +11,7 @@
     </Head>
     
     <!-- 轮播图 -->
-    <img class="main-header-image" src="http://xmswhycbhzx.cn/uploads/20251013/6af88ead615998ad6c789327b26f7bbe.jpg">
+    <img class="main-header-image" src="@/assets/images/banner/2.jpg">
 
     <!-- 主要内容 -->
     <div class="main-content">
@@ -25,17 +25,17 @@
               </div>
               
               <!-- 搜索区域 -->
-              <div class="d-flex justify-content-center align-items-center gap-3 mb-4">
+              <div class="d-flex justify-content-center flex-wrap align-items-center gap-3 mb-4">
                 <button 
                   v-for="value in channelData.content.value?.childs"
                   :key="value.id"
-                  class="bordered" 
+                  class="bordered flex-shrink-0" 
                   :class="{ 'active': selectedCategory === value.id }"
                   @click="selectCategory(value.id)"
                 >
                   {{ value.name }}
                 </button>
-                <div class="bordered-input">
+                <div class="bordered-input flex-shrink-0">
                   <input 
                     type="text" 
                     id="search-input" 
@@ -59,14 +59,7 @@
                     :key="key" 
                     class="news-item dark"
                   >
-                    <a v-if="item.outlink" :href="item.outlink" target="_blank">{{ item.title }}</a>
-                    <router-link 
-                      v-else
-                      :to="'/page/' + item.id"
-                      class="title"
-                    >
-                      {{ item.title }}
-                    </router-link>
+                    <PageLink :article="item" class="title" />
                     <span class="date">
                       {{ DateUtils.formatDate(new Date((item.publishtime || item.createtime) * 1000), 'yyyy-MM-dd') }}
                     </span>
@@ -100,6 +93,7 @@ import { useSSrSimpleDataLoader } from '@/composeable/SimpleDataLoader';
 import { DateUtils } from '@imengyu/imengyu-utils';
 import type { IChannel } from '~~/server/api/ecms/channel/[id]';
 import SimplePagination from '~/components/content/SimplePagination.vue';
+import PageLink from '~/components/link/PageLink.vue';
 
 const route = useRoute();
 const router = useRouter();

+ 245 - 85
src/pages/index.vue

@@ -29,17 +29,99 @@
       </template>
     </Carousel>
 
+    <h2 class="text-center banner-title">
+    </h2>
+
     <!-- 主要内容 -->
     <div class="main-content">
       <div class="container">
-        <!-- 通知公告和热点新闻 -->
+
+        <!-- 学习强国 -->
         <div class="row">
+          <div class="col col-12">
+            
+            <Carousel v-bind="carouselConfig2" class="main-box p-0">
+              <Slide class="d-flex flex-row w-100">
+                <a href="http://www.12371.cn/special/xxzd/" target="_blank">
+                  <img src="@/assets/images/bars/7.jpg" class="bars-image" /> 
+                </a>
+              </Slide>
+              <Slide class="d-flex flex-row w-100">
+                <a href="http://cpc.people.com.cn/GB/67481/448544/index.html" target="_blank">
+                  <img src="@/assets/images/bars/1.jpg" class="bars-image" /> 
+                </a>
+              </Slide>
+              <Slide class="d-flex flex-row w-100">
+                <a href="http://wlj.xm.gov.cn/ztzl/dsxxjy/" target="_blank">
+                  <img src="@/assets/images/bars/2.jpg" class="bars-image" /> 
+                </a>
+              </Slide>
+              <Slide class="d-flex flex-row w-100">
+                <a href="http://wlj.xm.gov.cn/ztzl/ggcx/" target="_blank">
+                  <img src="@/assets/images/bars/8.jpg" class="bars-image" /> 
+                </a>
+              </Slide>
+              <Slide class="d-flex flex-row w-100">
+                <a href="http://wlj.xm.gov.cn/ztzl/ggcx/" target="_blank">
+                  <img src="@/assets/images/bars/3.jpg" class="bars-image" /> 
+                </a>
+              </Slide>
+              <!-- <Slide class="d-flex flex-row w-100">
+                <a href="https://liuyan.www.gov.cn/2021wxzlsjh/bmdf.html?q=fj" target="_blank">
+                  <img src="@/assets/images/bars/4.jpg" class="bars-image" /> 
+                </a>
+              </Slide>
+              <Slide class="d-flex flex-row w-100">
+                <a href="https://wlj.xm.gov.cn/ztzl/202003/t20200326_2434593.htm" target="_blank">
+                  <img src="@/assets/images/bars/5.jpg" class="bars-image" /> 
+                </a>  
+              </Slide>
+              <Slide class="d-flex flex-row w-100">
+                <a href="http://xm.zwfw.fujian.gov.cn/person-todo/detail?userType=3&curStatus=dept&deptId=A8E0954E119F2E6342C87F48AC54EE31&bsfwCount=93&xzjgCount=609" target="_blank">
+                  <img src="@/assets/images/bars/6.jpg" class="bars-image" /> 
+                </a>
+              </Slide> -->
+              <template #addons>
+                <Navigation />
+                <Pagination />
+              </template>
+            </Carousel>
 
-          <div class="col col-sm-12 col-md-6 col-lg-6 notices-list">
-            <div class="section-title">
+          </div>
+        </div>
+        
+        <!-- 通知公告和热点新闻 -->
+        <div class="row mt-3">
+
+          <div class="col col-sm-12 col-md-6 col-lg-6">
+            <Carousel v-bind="carouselConfig">
+              <Slide 
+                v-for="(item, key) in recommendArticles.content.value"
+                :key="key"
+                class="featured-image bg-secondary"
+                :style="{ 'background-image': `url(${item.image})`, 'background-size': 'cover' }"
+              >
+                <router-link :to="'/page/' + item.id">
+                  <img :src="item.image" :alt="item.desc || item.title" style="width: 100%; height: 100%; object-fit: contain;">
+                  <div class="caption">
+                    <h3>{{ item.title }}</h3>
+                    <p>{{ item.desc || '' }}</p>
+                  </div>
+                </router-link>
+              </Slide>
+              <template #addons>
+                <Navigation />
+                <Pagination />
+              </template>
+            </Carousel>
+
+          </div>
+
+          <div class="d-flex flex-column col col-sm-12 col-md-6 col-lg-6 notices-list">
+            <!-- <div  class="section-title">
               <div class="d-flex flex-row align-items-center">
                 <h2 id="notices-button" @click="currentTab = 'notices'" :class="{'active': currentTab === 'notices'}" class="button icon">通知公告</h2>
-                <!-- <h2 id="hot-news-button" @click="currentTab = 'hot'" :class="{'active': currentTab === 'hot'}" class="button icon">热点新闻</h2> -->
+                <h2 id="hot-news-button" @click="currentTab = 'hot'" :class="{'active': currentTab === 'hot'}" class="button icon">热点新闻</h2>
               </div>
               <router-link :to="currentTab === 'notices' ? 
                 `/channel/${notices.content.value?.channel_id}` : 
@@ -47,68 +129,72 @@
                 class="section-more">
                 查看更多 <Icon name="material-symbols-light:chevron-right" />
               </router-link>
-            </div>
-
-            <div v-if="currentTab === 'notices'" id="notices-list" class="notices-content">
+            </div> -->
+            <!-- <div v-if="currentTab === 'notices'" id="notices-list" class="notices-content flex-one flex-grow-1">
               <SimplePageContentLoader :loader="notices" :showEmpty="!notices.content.value?.items?.length">
                 <div v-for="notice in notices.content.value?.items" :key="notice.id" class="notice-item">
                   <div>
-                    <router-link :to="'/page/' + notice.id" class="notice-title">{{ notice.title }}</router-link>
+                    <PageLink :article="notice" class="notice-title" />
                     <p>{{ notice.description }}</p>
                   </div>
                   <span class="notice-publishtime">{{ DataDateUtils.formatDate(new Date((notice.createtime || notice.publishtime) * 1000), 'yyyy-MM-dd') }}</span>
                 </div>
               </SimplePageContentLoader>
             </div>
-            <div v-if="currentTab === 'hot'" id="hot-news-list" class="news-content">
+            <div v-if="currentTab === 'hot'" id="hot-news-list" class="news-content flex-one flex-grow-1">
               <SimplePageContentLoader :loader="hot" :showEmpty="!hot.content.value?.items?.length">
                 <div v-for="item in hot.content.value?.items" :key="item.id" class="news-item">
-                  <router-link :to="'/page/' + item.id" class="title">{{ item.title }}</router-link>
+                  <PageLink :article="item" class="title" />
                   <span class="publishtime">{{ DataDateUtils.formatDate(new Date((item.createtime ||item.publishtime) * 1000), 'yyyy-MM-dd') }}</span>
                 </div>
               </SimplePageContentLoader>
+            </div> -->
+          
+            <div class="section-title">
+              <h2>
+                <Icon name="famicons:logo-designernews" />
+                工作动态
+              </h2>
+              <router-link :to="`/channel/${workUppublishtimes.content.value?.channel_id}`" class="section-more">查看更多 <Icon name="material-symbols-light:chevron-right" /></router-link>
+            </div>
+            <div class="news-content">
+              <SimplePageContentLoader :loader="workUppublishtimes" :showEmpty="!workUppublishtimes.content.value?.items?.length">
+                <div v-for="uppublishtime in workUppublishtimes.content.value?.items" :key="uppublishtime.id" class="news-item">
+                  <PageLink :article="uppublishtime" class="title" />
+                  <span class="publishtime">{{ DataDateUtils.formatDate(new Date(uppublishtime.publishtime * 1000), 'yyyy-MM-dd') }}</span>
+                </div>
+              </SimplePageContentLoader>
             </div>
-
           </div>
+        </div>
 
-          <div class="col col-sm-12 col-md-6 col-lg-6">
+        <!-- 工作动态和党建工作 -->
+        <div class="row mt-lg-3 mt-md-2 mt-sm-2">
+
+
+          <div class="col col-sm-12 col-md-4 col-lg-3 d-flex flex-column">
+            
             <Carousel v-bind="carouselConfig">
               <Slide 
-                v-for="(item, key) in recommendArticles.content.value"
+                v-for="(item, key) in carousel2Data.content.value"
                 :key="key"
                 class="featured-image bg-secondary"
                 :style="{ 'background-image': `url(${item.image})`, 'background-size': 'cover' }"
               >
-                <router-link :to="'/page/' + item.id">
-                  <img :src="item.image" :alt="item.desc || item.title" style="width: 100%; height: 100%; object-fit: contain;">
-                  <div class="caption">
-                    <h3>{{ item.title }}</h3>
-                    <p>{{ item.desc || '' }}</p>
-                  </div>
-                </router-link>
+                <img 
+                  :src="item.image" style="width: 100%; height: 100%; object-fit: contain;"
+                  @click="handleImageClick(item)"
+                >
               </Slide>
               <template #addons>
                 <Navigation />
                 <Pagination />
               </template>
             </Carousel>
-            <div class="d-flex flex-row justify-content-between">
-              <a href="http://cpc.people.com.cn/GB/67481/448544/index.html" class="w-50">
-                <img src="https://wlj.xm.gov.cn/ztzl/202211/W020221213510920336898.jpg" class="mt-3" style="width: 100%; height:130px;object-fit:cover;" /> 
-              </a>
-              <a href="https://www.12371.cn/special/xxzd/" class="w-50">
-                <img src="@/assets/images/xuexi.png" class="mt-3" style="width: 100%; height:130px;object-fit:cover;" /> 
-              </a>
-            </div>
           </div>
-        </div>
-
-        <!-- 工作动态和党建工作 -->
-        <div class="row mt-lg-3 mt-md-2 mt-sm-2">
-
-          <div class="col col-sm-12 col-md-8 col-lg-9">
+          <div class="col col-sm-12 col-md-8 col-lg-9 d-flex flex-column">
             <!-- 工作动态 -->
-            <div class="main-box">
+            <!-- <div class="main-box">
               <div class="section-title">
                 <h2>
                   <Icon name="material-symbols-light:flag" />
@@ -124,9 +210,9 @@
                   </div>
                 </SimplePageContentLoader>
               </div>
-            </div>
+            </div> -->
             <!-- 党建工作 -->
-            <div class="main-box mt-lg-3 mt-md-2 mt-sm-2">
+            <!-- <div class="main-box mt-lg-3 mt-md-2 mt-sm-2">
               <div class="section-title">
                 <h2>
                   <Icon name="famicons:logo-designernews" />
@@ -137,12 +223,40 @@
               <div class="news-content">
                 <SimplePageContentLoader :loader="workUppublishtimes" :showEmpty="!workUppublishtimes.content.value?.items?.length">
                   <div v-for="uppublishtime in workUppublishtimes.content.value?.items" :key="uppublishtime.id" class="news-item">
-                    <router-link :to="'/page/' + uppublishtime.id" class="title">{{ uppublishtime.title }}</router-link>
+                    <PageLink :article="uppublishtime" class="title" />
                     <span class="publishtime">{{ DataDateUtils.formatDate(new Date(uppublishtime.publishtime * 1000), 'yyyy-MM-dd') }}</span>
                   </div>
                 </SimplePageContentLoader>
               </div>
+            </div> -->
+
+            <!-- 通知公告 -->
+            <div class="main-box flex-fill">
+              <div class="section-title">
+                <h2>
+                  <Icon name="famicons:logo-designernews" />
+                  通知公告
+                </h2>
+                <router-link :to="`/channel/${notices.content.value?.channel_id}`" class="section-more">查看更多 <Icon name="material-symbols-light:chevron-right" /></router-link>
+              </div>
+              <div class="notices-content flex-one flex-grow-1">
+                <SimplePageContentLoader :loader="notices" :showEmpty="!notices.content.value?.items?.length">
+                  <div v-for="notice in notices.content.value?.items" :key="notice.id" class="notice-item">
+                    <div>
+                      <PageLink :article="notice" class="notice-title" />
+                      <p>{{ notice.description }}</p>
+                    </div>
+                    <span class="notice-publishtime">{{ DataDateUtils.formatDate(new Date((notice.createtime || notice.publishtime) * 1000), 'yyyy-MM-dd') }}</span>
+                  </div>
+                </SimplePageContentLoader>
+              </div>
             </div>
+          </div>
+        </div>
+
+        <!-- 精彩推荐 -->
+        <div class="row mt-lg-3 mt-md-2 mt-sm-2">
+          <div class="col col-12">
             <!-- 精彩推荐 -->
             <div class="main-box mt-lg-3 mt-md-2 mt-sm-2">
               <div class="section-title">
@@ -161,31 +275,6 @@
               </div>
             </div>
           </div>
-
-          <div class="col col-sm-12 col-md-4 col-lg-3 d-flex flex-column">
-            
-            <Carousel v-bind="carouselConfig">
-              <Slide 
-                v-for="(item, key) in carousel2Data.content.value"
-                :key="key"
-                class="featured-image bg-secondary"
-                :style="{ 'background-image': `url(${item.image})`, 'background-size': 'cover' }"
-              >
-                <img 
-                  :src="item.image" style="width: 100%; height: 100%; object-fit: contain;"
-                  @click="handleImageClick(item)"
-                >
-              </Slide>
-              <template #addons>
-                <Navigation />
-                <Pagination />
-              </template>
-            </Carousel>
-
-            <div class="main-box mt-lg-3 mt-md-2 mt-sm-2 flex-fill">
-
-            </div>
-          </div>
         </div>
 
         <hr class="main-hr" />
@@ -198,51 +287,77 @@
             <div class="service-card" @click="handleServiceClick('volunteer')">
               <img class="icon" src="@/assets/images/icon-explore.png">
               <h3>志愿者招募</h3>
-              <a href="#" class="service-link">
+              <div class="service-link">
                 <Icon name="material-symbols-light:chevron-right" />
-              </a>
+              </div>
             </div>
 
             <div class="service-card" @click="handleServiceClick('activity')">
               <img class="icon" src="@/assets/images/icon-join.png">
               <h3>活动报名</h3>
-              <a href="#" class="service-link">
+              <div class="service-link">
                 <Icon name="material-symbols-light:chevron-right" />
-              </a>
+              </div>
             </div>
 
-            <div class="service-card" @click="handleServiceClick('contact')">
+            <div class="service-card" @click="router.push('/about/')">
               <img class="icon" src="@/assets/images/icon-contract.png">
               <h3>联系咨询通道</h3>
-              <a href="#" class="service-link">
+              <RouterLink to="/about/" class="service-link">
                 <Icon name="material-symbols-light:chevron-right" />
-              </a>
+              </RouterLink>
             </div>
           </div>
         </div>
+
+        <!-- 友情链接 -->
+        <LinkInfos showType="list" />
       </div>
     </div>
+
+    <!-- 扫码进入小程序对话框 -->
+    <AModal
+      v-model:open="qrModalVisible"
+      :title="qrModalTitle"
+      :footer="null"
+      centered
+    >
+      <div class="qr-modal-content">
+        <p class="qr-modal-tip">请扫码进入小程序</p>
+        <img v-if="qrImageSrc" :src="qrImageSrc" alt="小程序二维码" class="qr-modal-img">
+      </div>
+    </AModal>
   </div>
 </template>
 
 <script setup lang="ts">
 import { Carousel, Slide, Pagination, Navigation } from 'vue3-carousel'
-import { ref } from 'vue';
+import { ref, computed } from 'vue';
 import { useSSrSimpleDataLoader } from '@/composeable/SimpleDataLoader';
 import { DataDateUtils } from '@imengyu/js-request-transform';
 import { MemoryTimeOut } from '@imengyu/imengyu-utils';
+import volunteerQr from '@/assets/images/qr1/volunteer.png';
+import activityQr from '@/assets/images/qr1/activity.png';
 import CarouselItem1 from '@/assets/images/board1/1.jpg';
 import CarouselItem2 from '@/assets/images/board1/2.jpg';
 import CarouselItem3 from '@/assets/images/board1/3.jpg';
 import CarouselItem4 from '@/assets/images/board1/4.jpg';
 import CarouselItem5 from '@/assets/images/board1/5.jpg';
 import type { ICarousel } from '~~/server/api/ecms/carousel';
+import { useRouter } from 'vue-router';
 
+const router = useRouter();
 const carouselConfig = {
   itemsToShow: 1,
   wrapAround: true,
   autoplay: 5000,
 }
+const carouselConfig2 = {
+  itemsToShow: 1,
+  wrapAround: true,
+  autoplay: 5000,
+  mouseDrag: false,
+} as typeof Carousel['props'];
 const carouselData = await useSSrSimpleDataLoader('carousel', async () => {
   const res = await $fetch(`/api/ecms/carousel?name=banner`);
   if (!res.status)
@@ -272,7 +387,7 @@ const notices = await useSSrSimpleDataLoader('notices', async () => {
 });
 // 工作动态数据
 const workUppublishtimes = await useSSrSimpleDataLoader('workUppublishtimes', async () => {
-  const res = await $fetch(`/api/ecms/article/byChannelName?channelName=工作动态`);
+  const res = await $fetch(`/api/ecms/article/byChannelName?channelName=工作动态&bannedChannels=新闻公告`);
   if (!res.status)
     throw new Error(res.message);
   return res.data;
@@ -293,39 +408,84 @@ const hot = await useSSrSimpleDataLoader('hot', async () => {
 });
 // 精彩推荐数据
 const featured = await useSSrSimpleDataLoader('featured', async () => {
-  const res = await $fetch(`/api/ecms/article/byChannelName?channelName=鹭岛文脉&pageSize=9`);
+  const res = await $fetch(`/api/ecms/article/byChannelName?channelName=非遗流韵,文保家园&pageSize=12`);
   if (!res.status)
     throw new Error(res.message);
   return res.data;
 });
 
+const qrModalVisible = ref(false);
+const qrModalType = ref<'volunteer' | 'activity'>('volunteer');
+const qrModalTitle = computed(() => qrModalType.value === 'volunteer' ? '志愿者招募' : '活动报名');
+const qrImageSrc = computed(() => qrModalType.value === 'volunteer' ? volunteerQr : activityQr);
+
 function handleImageClick(image: ICarousel) {
   window.open(image.url || image.image, '_blank');
 }
-function handleServiceClick(service: string) {
-  Modal.info({
-    title: '提示',
-    content: '网站正在进行内部测试,暂未对外开放功能,敬请期待。',
-  });
+function handleServiceClick(service: 'volunteer' | 'activity') {
+  qrModalType.value = service;
+  qrModalVisible.value = true;
 }
 
 
-
-const tipCounter = new MemoryTimeOut('devtip', 1000 * 3600);
-
 onMounted(() => {
-  if (tipCounter.isTimeout()) {
-    tipCounter.recordTime();
+  if (new Date().getTime() < new Date('2026-04-22 04:00:00').getTime())
     Modal.info({
       title: '提示',
       content: '网站正在进行内部测试,暂未对外开放。',
     })
-  }
 });
 </script>
 
-<style lang="scss">
+<style lang="scss" scoped>
 @use "sass:list";
 @use "sass:math";
+
+.banner-title {
+
+  $height: 65px;
+
+  font-family: "STSongtiSC", "Microsoft YaHei", sans-serif;
+  font-size: $height;
+  font-weight: bold;
+  letter-spacing: 10px;
+  color: #fff;
+  text-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
+  margin-top: 35px;
+  margin-bottom: 20px;
+  position: absolute;
+  top: 80px;
+  left: 0;
+  right: 0;
+  width: 100%;
+  text-align: center;
+  z-index: 10;
+  min-height: $height;
+  background-image: url('@/assets/images/index-title.png');
+  background-size: auto $height;
+  background-repeat: no-repeat;
+  background-position: center center;
+}
+.bars-image {
+  width: 100%;
+  height: 130px;
+  object-fit: contain;
+}
+.qr-modal-content {
+  text-align: center;
+  padding: 8px 0;
+  .qr-modal-tip {
+    margin-bottom: 16px;
+    color: rgba(0, 0, 0, 0.85);
+    font-size: 14px;
+  }
+  .qr-modal-img {
+    max-width: 100%;
+    width: 240px;
+    height: auto;
+    display: block;
+    margin: 0 auto;
+  }
+}
 </style>
 

+ 2 - 0
src/pages/inheritor/artifact.vue

@@ -10,6 +10,8 @@
     :tagsData="tagsData"
     :defaultSelectTag="tagsData[0]?.id"
     detailsPage="/inheritor/details/artifact"
+    :showCarousel="true"
+    carouselName="banner-intangible"
   />
 </template>
 

+ 2 - 0
src/pages/inheritor/inheritor.vue

@@ -17,6 +17,8 @@
     :detailsParams="{
       groupTitle: '非遗传承人',
     }"
+    :showCarousel="true"
+    carouselName="banner-intangible"
   >
     <template #contentPre>
       <HeaderBox>

+ 29 - 23
src/pages/inheritor/intangible.vue

@@ -17,6 +17,8 @@
     :detailsParams="{
       groupTitle: '非遗项目',
     }"
+    :showCarousel="true"
+    carouselName="banner-intangible"
   >
     <template #contentPre>
       <HeaderBox>
@@ -96,31 +98,35 @@ const statsItemLoader =  useSimpleDataLoader(async () => {
 });
 
 onMounted(async () => {
-  tagsData.value = tagsData.value.concat((await CommonContent.getCategoryList(4)).map((item) => ({
-    id: item.id,
-    name: item.title,
-  })));
-  const levels = await CommonContent.getCategoryList(2);
-  dropdownNames.value.push({ 
-    options: [{
-      id: 0, 
-      name: '全部'
-    }].concat(levels.map((item) => ({
+  try {
+    tagsData.value = tagsData.value.concat((await CommonContent.getCategoryList(4)).map((item) => ({
       id: item.id,
       name: item.title,
-    }))),
-    defaultSelectedValue: route.query.level ? Number(route.query.level) : 0,
-  });
-  dropdownNames.value.push({ 
-    options: [{
-      id: 0, 
-      name: '全部'
-    }].concat((await CommonContent.getCategoryList(1)).map((item) => ({
-      id: item.id,
-      name: item.title,
-    }))),
-    defaultSelectedValue: 0,
-  });
+    })));
+    const levels = await CommonContent.getCategoryList(2);
+    dropdownNames.value.push({ 
+      options: [{
+        id: 0, 
+        name: '全部'
+      }].concat(levels.map((item) => ({
+        id: item.id,
+        name: item.title,
+      }))),
+      defaultSelectedValue: route.query.level ? Number(route.query.level) : 0,
+    });
+    dropdownNames.value.push({ 
+      options: [{
+        id: 0, 
+        name: '全部'
+      }].concat((await CommonContent.getCategoryList(1)).map((item) => ({
+        id: item.id,
+        name: item.title,
+      }))),
+      defaultSelectedValue: 0,
+    });
+  } catch (error) {
+    console.error('error', error);
+  }
 })
 </script>
 

+ 2 - 0
src/pages/inheritor/seminar.vue

@@ -6,6 +6,8 @@
     :pageSize="8"
     :load="loadData"
     :loadDetail="loadDetail"
+    :showCarousel="true"
+    carouselName="banner-intangible"
   />
 </template>
 

+ 8 - 2
src/pages/page/[id].vue

@@ -71,6 +71,11 @@
                   <div class="content" v-html="articlesData.content.value.content">
                   </div>
                   <video v-if="articlesData.content.value.video" :src="articlesData.content.value.video" controls></video>
+                  <iframe 
+                    v-if="articlesData.content.value.document_file" :src="articlesData.content.value.document_file"
+                    class="w-100"
+                    style="height: 80vh;"
+                  ></iframe>
                   <a-empty v-if="isEmpty"
                     description="暂无内容"
                   />
@@ -109,7 +114,7 @@ const carouselData = await useSSrSimpleDataLoader('carousel', async () => {
 const articlesData = await useSSrSimpleDataLoader('articles' + articleId, async () => {
   const res = await $fetch(`/api/ecms/article/${articleId}`, {
     method: 'GET',
-  });
+  });  
   if (!res.status)
     throw new Error(res.message);
   return res.data;
@@ -118,7 +123,8 @@ const articlesData = await useSSrSimpleDataLoader('articles' + articleId, async
 const isEmpty = computed(() => articlesData.content.value 
   && !articlesData.content.value.content 
   && !articlesData.content.value.image 
-  && !articlesData.content.value.video);
+  && !articlesData.content.value.video
+  && !articlesData.content.value.document_file);
 
 watch(() => route.query.id, async (newVal, oldVal) => {
   if (newVal !== oldVal) {