Procházet zdrojové kódy

📦 按要求调整栏目和细节

快乐的梦鱼 před 1 měsícem
rodič
revize
636b2c5fd2

+ 3 - 0
nuxt.config.ts

@@ -50,6 +50,9 @@ export default defineNuxtConfig({
   },
   routeRules: {
     '/**': { swr: false, isr: false, headers: { 'cache-control': 'no-store, max-age=0' } },
+    '/uploads/**': {
+      proxy: 'https://xmswhycbhzx.cn/uploads/**'
+    },
     
     /*
     '/': { swr: 1800 },

+ 4 - 4
package-lock.json

@@ -9,7 +9,7 @@
       "version": "0.0.0",
       "dependencies": {
         "@ant-design-vue/nuxt": "^1.4.6",
-        "@imengyu/imengyu-utils": "^0.0.25",
+        "@imengyu/imengyu-utils": "^0.0.26",
         "@imengyu/js-request-transform": "^0.4.0",
         "@imengyu/vue-dynamic-form": "^0.1.1",
         "@imengyu/vue-scroll-rect": "^0.1.8",
@@ -1198,9 +1198,9 @@
       }
     },
     "node_modules/@imengyu/imengyu-utils": {
-      "version": "0.0.25",
-      "resolved": "https://registry.npmmirror.com/@imengyu/imengyu-utils/-/imengyu-utils-0.0.25.tgz",
-      "integrity": "sha512-xs+8dLnG4o4ssPgtoRUfUq8n64yoyj0rAGQlDsxWy+4BBdEpRowPtznlcaEHWnM5p6PukVfWTr0JVkzKhTa2Vg==",
+      "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==",
       "license": "MIT",
       "dependencies": {
         "@imengyu/js-request-transform": "^0.3.6"

+ 1 - 1
package.json

@@ -15,7 +15,7 @@
   },
   "dependencies": {
     "@ant-design-vue/nuxt": "^1.4.6",
-    "@imengyu/imengyu-utils": "^0.0.25",
+    "@imengyu/imengyu-utils": "^0.0.26",
     "@imengyu/js-request-transform": "^0.4.0",
     "@imengyu/vue-dynamic-form": "^0.1.1",
     "@imengyu/vue-scroll-rect": "^0.1.8",

+ 21 - 9
server/api/ecms/article/byChannel.ts

@@ -10,17 +10,29 @@ export default defineEventHandler<EventHandlerRequest, Promise<IResponse<CommonP
     const page = query.page as string;
     const pageSize = query.pageSize as string;
     const channelId = query.channelId as string;
+    const includeChilds = query.includeChilds === 'true';
     if (!channelId)
       return createErrorResponse('分类ID不能为空');
-    const articles = await DB.table('pr_cms_archives')
-            .whereNull('deletetime')
-            .where('channel_id', channelId)
-            .where('status', 'normal')
-            .orderBy('weigh', 'desc')
-            .orderBy('publishtime', 'desc')
-            .orderBy('createtime', 'desc')
-            .orderBy('id', 'asc')
-            .paginate(Number(page), Number(pageSize));
+
+    const dbquery = DB.table('pr_cms_archives')
+      .whereNull('deletetime')
+      .where('status', 'normal')
+      .orderBy('weigh', 'desc')
+      .orderBy('publishtime', 'desc')
+      .orderBy('createtime', 'desc')
+      .orderBy('id', 'asc');
+
+    if (includeChilds) {
+      const childChannels = await DB.table('pr_cms_channel')
+        .where('parent_id', channelId)
+        .select('id')
+        .get();
+      dbquery.whereIn('channel_id', [channelId, ...childChannels.map(item => item.id)]);
+    } else {
+      dbquery.where('channel_id', channelId);
+    }
+
+    const articles = await dbquery.paginate(Number(page), Number(pageSize));
     if (!articles) 
       return createErrorResponse('文章不存在');
     return createSuccessResponse(articles);

+ 8 - 3
server/api/ecms/article/byChannelName.ts

@@ -24,10 +24,15 @@ export default defineEventHandler<EventHandlerRequest, Promise<IResponse<ICommon
     if (!channel) 
       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 channelId = channel.id;
     const articles = await DB.table('pr_cms_archives')
-            .where('channel_id', channelId)
+            .whereIn('channel_id', [channel.id, ...subChannelIds])
             .where('status', 'normal')
             .whereNull('deletetime')
             .orderBy('weigh', 'desc')
@@ -39,7 +44,7 @@ export default defineEventHandler<EventHandlerRequest, Promise<IResponse<ICommon
       return createErrorResponse('文章不存在');
 
     return createSuccessResponse({
-      channel_id: channelId,
+      channel_id: channel.id,
       ...articles
     });
   } catch (error) {

+ 0 - 9
src/App.vue

@@ -35,15 +35,6 @@ onMounted(() => {
   if (import.meta.server)
     return;
 });
-
-const route = useRoute();
-
-watch(route, () => {
-  window.scrollTo({
-    top: 0,
-    behavior: 'instant'
-  })
-});
 </script>
 
 <style>

+ 37 - 2
src/assets/scss/main.scss

@@ -65,9 +65,12 @@ ul {
 
 /* 头部样式 */
 header {
+  position: sticky;
   padding: 15px 0;
   background-color: #fff;
   box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
+  top: 0;
+  z-index: 100;
 
   .inner {
     display: flex;
@@ -263,7 +266,7 @@ nav.main-nav {
 }
 .main-header-image {
   width: 100%;
-  height: 500px;
+  height: 400px;
   object-fit: cover;
 }
 
@@ -463,8 +466,37 @@ nav.main-nav {
     }
   }
 
-  .title {
+  .no-image,
+  img {
+    width: 100px;
+    height: 70px;
+    object-fit: contain;
+    border-radius: 5px;
+    margin-right: 15px;
+    background-color: var(--color-border-split);
+  }
+
+  .no-image {
+    font-size: 11px;
+    color: var(--color-text-secondary);
+    display: flex;
+    justify-content: center;
+    align-items: center;
+  }
+
+  .content {
     flex: 1;
+    display: flex;
+    flex-direction: row;
+
+    .desc {
+      display: flex;
+      flex-direction: column;
+      justify-content: center;
+    }
+  }
+
+  .title {
     margin-right: 15px;
     white-space: nowrap;
     overflow: hidden;
@@ -479,6 +511,9 @@ nav.main-nav {
     color: #999;
     font-size: 14px;
   }
+  p {
+    margin: 0;
+  }
 }
 
 /* 精彩推荐区域 */

+ 0 - 1
src/assets/scss/news.scss

@@ -8,7 +8,6 @@
   position: relative;
   display: flex;
   flex-direction: column;
-  row-gap: 20px;
 
   &.grid {
     row-gap: 0;

+ 5 - 2
src/components/NavBar.vue

@@ -41,14 +41,17 @@ const route = useRoute();
 
 const navItems = await useSSrSimpleDataLoader('navItems', async () => {
   const data = (await $fetch('/api/ecms/channel/nav')).data || [];
-  return solveChannelData(data);
+  const items = solveChannelData(data);
+  return items;
 });
 const isMenuOpen = ref(false);
 const isSearchOpen = ref(false);
 const keyword = ref('');
 
 function isActive(url: string): boolean {
-  return route.path.startsWith(url);
+  if (url.includes('?'))
+    url = url.split('?')[0] || '';
+  return route.path.startsWith(url) || route.path === url;
 }
 function toggleMenu() {
   isMenuOpen.value = !isMenuOpen.value;

+ 4 - 2
src/components/PageContainer.vue

@@ -4,8 +4,10 @@
     <!-- SEO -->
     <Head>
       <Title>厦门市文化遗产保护中心 - {{ title }}</Title>
-      <Meta name="description" content="" />
-      <Meta name="keywords" content="" />
+      <Meta name="description" :content="title" />
+      <Meta name="keywords" content="厦门市文化遗产保护,厦门历史建筑保护,鼓浪屿文化遗产,南音艺术传承,厦门传统手工艺,厦门非物质文化遗产,海沧民俗文化保护,厦门古迹游览,文化遗产教育推广,厦门历史文化研究,闽南文化保护中心,厦门文物保护项目,文化遗产修复技术,厦门民间艺术展示,厦门文化节庆活动" />
+      <Meta name="author" content="厦门市文化遗产保护中心" />
+      <Meta name="robots" content="index, follow" />
     </Head>
     <!-- 轮播 -->
     <slot name="carousel" />

+ 3 - 1
src/components/Sidebar.vue

@@ -23,7 +23,9 @@
           </div>
         </router-link>
       </li>
-      <li v-if="showEmpty && (!items || items.length === 0)" class="no-content">暂无相关子分类</li>
+      <li v-else-if="showEmpty && (!items || items.length === 0)" class="no-content">
+        暂无相关子分类
+      </li>
     </ul>
   </div>
 </template>

+ 4 - 3
src/composeable/SimpleDataLoader.ts

@@ -1,5 +1,6 @@
 import { onMounted, ref, type Ref } from "vue";
 import type { ILoaderCommon, LoaderLoadType } from "./LoaderCommon";
+import { formatError } from "@imengyu/imengyu-utils";
 
 export interface ISimpleDataLoader<T, P> extends ILoaderCommon<P> {
   content: Ref<T|null>;
@@ -34,7 +35,7 @@ export function useSimpleDataLoader<T, P = any>(
     } catch(e) {
       if (import.meta.env.DEV)
         console.error(e);
-      loadError.value = '' + e;
+      loadError.value = formatError(e);
       loadStatus.value = 'error';
       console.log(e);
       
@@ -88,7 +89,7 @@ export async function useSSrSimpleDataLoader<T, P = any>(
         loadStatus.value = 'finished';
       loadError.value = '';
     } catch(e) {
-      loadError.value = '' + e;
+      loadError.value = formatError(e);
       loadStatus.value = 'error';
       console.log(e);
     }
@@ -96,7 +97,7 @@ export async function useSSrSimpleDataLoader<T, P = any>(
 
   watch(error, (e) => {
     if (e) {
-      loadError.value = '' + e;
+      loadError.value = formatError(e);
       loadStatus.value = 'error';
     }
   }, { immediate: true });

+ 4 - 3
src/composeable/SimplePagerDataLoader.ts

@@ -1,5 +1,6 @@
 import { watch, ref, computed, type Ref } from "vue"
 import type { ILoaderCommon, LoaderLoadType } from "./LoaderCommon";
+import { formatError } from "@imengyu/imengyu-utils";
 
 export interface ISimplePageListLoader<T, P> extends ILoaderCommon<P> {
   list: Ref<T[]>;
@@ -80,7 +81,7 @@ export function useSimplePagerDataLoader<T, P = any>(
     } catch(e) {
       if (import.meta.env.DEV)
         console.error(e);
-      loadError.value = '' + e;
+      loadError.value = formatError(e);
       loadStatus.value = 'error';
       loading = false;
     }
@@ -157,7 +158,7 @@ export async function useSSrSimplePagerDataLoader<T, P = any>(
   const loadError = ref('');
 
   if (error.value) {
-    loadError.value = '' + (error.value);
+    loadError.value = formatError(error.value);
     loadStatus.value = 'error';
   } else if (data.value) {
     list.value = data.value.data as any;
@@ -198,7 +199,7 @@ export async function useSSrSimplePagerDataLoader<T, P = any>(
     } catch(e) {
       if (import.meta.env.DEV)
         console.error(e);
-      loadError.value = '' + e;
+      loadError.value = formatError(e);
       loadStatus.value = 'error';
       loading = false;
     }

+ 13 - 2
src/pages/channel/[id].vue

@@ -45,8 +45,15 @@
       <SimplePageContentLoader :loader="articlesData">
         <div class="news-list">
           <div v-for="(item, key) in articlesData.content.value?.items" :key="key" class="news-item">
-            <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>
+            <div class="content">
+              <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>
+                <p class="desc">{{ item.description }}</p>
+              </div>
+            </div>
             <span class="date">{{ DateUtils.formatDate(new Date((item.createtime ||item.publishtime) * 1000), 'yyyy-MM-dd') }}</span>
           </div>
           <div v-if="!articlesData.content.value || articlesData.content.value.empty" class="no-news">暂无相关文章</div>
@@ -118,6 +125,7 @@ const articlesData = await useSSrSimpleDataLoader('articles' + channelId, async
       channelId: channelId,
       page: route.query.page || 1,
       pageSize: 10,
+      includeChilds: 'true',
     }
   });
   if (!res.status)
@@ -139,6 +147,9 @@ const articlesData = await useSSrSimpleDataLoader('articles' + channelId, async
   }
   return res.data;
 });
+const anyItemHasImage = computed(() => {
+  return articlesData.content.value?.items?.some(item => item.image);
+});
 
 const channelName = computed(() => channelData.content.value?.name || '');
 

+ 4 - 2
src/pages/channel/laws.vue

@@ -4,8 +4,10 @@
     <!-- SEO -->
     <Head>
       <Title>厦门市文化遗产保护中心 - {{ channelName }}</Title>
-      <Meta name="description" content="" />
-      <Meta name="keywords" content="" />
+      <Meta name="description" :content="`厦门市文化遗产保护中心${channelName}`" />
+      <Meta name="keywords" :content="`厦门市文化遗产保护,厦门历史建筑保护,鼓浪屿文化遗产,南音艺术传承,厦门传统手工艺,厦门非物质文化遗产,海沧民俗文化保护,厦门古迹游览,文化遗产教育推广,厦门历史文化研究,闽南文化保护中心,厦门文物保护项目,文化遗产修复技术,厦门民间艺术展示,厦门文化节庆活动,${channelName}`" />
+      <Meta name="author" content="厦门市文化遗产保护中心" />
+      <Meta name="robots" content="index, follow" />
     </Head>
     
     <!-- 轮播图 -->

+ 5 - 3
src/pages/index.vue

@@ -4,8 +4,10 @@
     <!-- SEO -->
     <Head>
       <Title>厦门市文化遗产保护中心</Title>
-      <Meta name="description" content="" />
-      <Meta name="keywords" content="" />
+      <Meta name="description" content="厦门市文化遗产保护中心是厦门市文化遗产保护的重要机构,致力于保护和传承厦门的历史文化遗产,包括历史建筑、传统手工艺、非物质文化遗产等。" />
+      <Meta name="keywords" content="厦门市,厦门市文化遗产保护,厦门历史建筑保护,鼓浪屿文化遗产,南音艺术传承,厦门传统手工艺,厦门非物质文化遗产,海沧民俗文化保护,厦门古迹游览,文化遗产教育推广,厦门历史文化研究,闽南文化保护中心,厦门文物保护项目,文化遗产修复技术,厦门民间艺术展示,厦门文化节庆活动" />
+      <Meta name="author" content="厦门市文化遗产保护中心" />
+      <Meta name="robots" content="index, follow" />
     </Head>
     <!-- 轮播 -->
     <Carousel v-bind="carouselConfig">
@@ -291,7 +293,7 @@ const hot = await useSSrSimpleDataLoader('hot', async () => {
 });
 // 精彩推荐数据
 const featured = await useSSrSimpleDataLoader('featured', async () => {
-  const res = await $fetch(`/api/ecms/article/byChannelName?channelName=热门新闻`);
+  const res = await $fetch(`/api/ecms/article/byChannelName?channelName=鹭岛文脉&pageSize=9`);
   if (!res.status)
     throw new Error(res.message);
   return res.data;

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

@@ -4,8 +4,10 @@
     <!-- SEO -->
     <Head>
       <Title>厦门市文化遗产保护中心 - {{ articlesData.content.value?.title }}</Title>
-      <Meta name="description" content="" />
-      <Meta name="keywords" content="" />
+      <Meta name="description" :content="articlesData.content.value?.description" />
+      <Meta name="keywords" :content="articlesData.content.value?.keywords" />
+      <Meta name="author" :content="articlesData.content.value?.title" />
+      <Meta name="robots" content="index, follow" />
     </Head>
     <!-- 轮播 -->
     <Carousel v-bind="carouselConfig" class="main-header-image carousel-light">