소스 검색

🎨 优化修改细节

快乐的梦鱼 1 개월 전
부모
커밋
0df58f150a

+ 33 - 0
package-lock.json

@@ -10,6 +10,7 @@
       "dependencies": {
         "@imengyu/imengyu-utils": "^0.0.15",
         "@imengyu/js-request-transform": "^0.3.5",
+        "@imengyu/vue-dynamic-form": "^0.1.2",
         "@imengyu/vue-scroll-rect": "^0.1.7",
         "@inquirer/prompts": "^7.8.3",
         "@vuemap/vue-amap": "^2.1.16",
@@ -2172,6 +2173,17 @@
         "dayjs": "^1.11.7"
       }
     },
+    "node_modules/@imengyu/vue-dynamic-form": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/@imengyu/vue-dynamic-form/-/vue-dynamic-form-0.1.2.tgz",
+      "integrity": "sha512-Q49Q0BcwkZXqi2gZ0+CUHLBSuDwPETH0HfuBqdfO58bCr/nwgiXX8ermL6qZA97u2casym5IUX3U/4ZDeKsoWQ==",
+      "license": "MIT",
+      "dependencies": {
+        "async-validator": "^4.2.5",
+        "scroll-into-view-if-needed": "^3.0.3",
+        "vue": "^3.2.45"
+      }
+    },
     "node_modules/@imengyu/vue-scroll-rect": {
       "version": "0.1.7",
       "resolved": "https://registry.npmmirror.com/@imengyu/vue-scroll-rect/-/vue-scroll-rect-0.1.7.tgz",
@@ -3737,6 +3749,12 @@
       "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
       "license": "MIT"
     },
+    "node_modules/async-validator": {
+      "version": "4.2.5",
+      "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
+      "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
+      "license": "MIT"
+    },
     "node_modules/asynckit": {
       "version": "0.4.0",
       "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -4207,6 +4225,12 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/compute-scroll-into-view": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz",
+      "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==",
+      "license": "MIT"
+    },
     "node_modules/confbox": {
       "version": "0.2.2",
       "resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.2.2.tgz",
@@ -6875,6 +6899,15 @@
       "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==",
       "license": "ISC"
     },
+    "node_modules/scroll-into-view-if-needed": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz",
+      "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==",
+      "license": "MIT",
+      "dependencies": {
+        "compute-scroll-into-view": "^3.0.2"
+      }
+    },
     "node_modules/sdk-base": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/sdk-base/-/sdk-base-2.0.1.tgz",

+ 1 - 0
package.json

@@ -14,6 +14,7 @@
   "dependencies": {
     "@imengyu/imengyu-utils": "^0.0.15",
     "@imengyu/js-request-transform": "^0.3.5",
+    "@imengyu/vue-dynamic-form": "^0.1.2",
     "@imengyu/vue-scroll-rect": "^0.1.7",
     "@inquirer/prompts": "^7.8.3",
     "@vuemap/vue-amap": "^2.1.16",

+ 6 - 1
src/App.vue

@@ -1,12 +1,17 @@
 <script setup lang="ts">
 import { RouterView } from 'vue-router'
 import { useTvFocusImprovement } from './composeable/TvFocusImprovement';
+import { KeepAlive } from 'vue';
 
 useTvFocusImprovement(true);
 </script>
 
 <template>
-  <RouterView />
+  <RouterView v-slot="{ Component }">
+    <KeepAlive include="HomeView">
+      <component :is="Component" />
+    </KeepAlive>
+  </RouterView>
 </template>
 
 <style scoped>

+ 7 - 3
src/api/CommonContent.ts

@@ -1,4 +1,4 @@
-import { DataModel, transformArrayDataModel, type NewDataModel } from '@imengyu/js-request-transform';
+import { DataModel, transformArrayDataModel, transformDataModel, type NewDataModel } from '@imengyu/js-request-transform';
 import { AppServerRequestModule } from './RequestModules';
 import ApiCofig from '@/common/config/ApiCofig';
 import type { QueryParams } from "@imengyu/imengyu-utils/dist/request";
@@ -429,8 +429,12 @@ export class CommonContentApi extends AppServerRequestModule<DataModel> {
       model_id: modelId ?? this.modelId,
       id,
       ...querys,
-    }, modelClassCreator)
-      .then(res => res.data as T)
+    })
+      .then(res => {
+        if (res.data === null || res.data instanceof Array)
+          throw new Error(`${this.debugName} (${id}) 内容详情返回空数据`); 
+        return transformDataModel<T>(modelClassCreator, res.data as T);
+      })
       .catch(e => { throw e });
   }
 

+ 31 - 0
src/api/introduction/IndexContent.ts

@@ -39,6 +39,25 @@ export class IndexBanner extends DataModel<IndexBanner> {
   openTypeText = '';
   statusText = '';
 }
+export class IndexTerminalConfig extends DataModel<IndexTerminalConfig> {
+  constructor() {
+    super(IndexTerminalConfig, "终端按钮配置");
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {}
+  }
+
+  id = '';
+  name = '';
+  images = [] as string[];
+  pageJson = [] as {
+    title: string,
+    logo: string,
+    page: string,
+  }[];
+  inheritorId = 0;
+  ichId = 0;
+}
+
 
 
 export class IndexContentApi extends CommonContentApi {
@@ -60,6 +79,18 @@ export class IndexContentApi extends CommonContentApi {
       '首页轮播图列表'
     );
   }
+  async bindMachineCode(userName: string, deviceCode: string) {
+    return (await this.post('/resource/site_bind/bind/bindMachineCode', {
+      username: userName,
+      device_code: deviceCode,
+    }, '绑定机器码')).data as any;
+  }
+  async getTerminalConfig(deviceCode: string) {
+    return (await this.get('/resource/site_bind/getInfo/getTerminalConfig', '获取终端按钮配置', {
+      device_code: deviceCode,
+    }, IndexTerminalConfig)).data as IndexTerminalConfig
+  }
+
 }
 
 export default new IndexContentApi();

+ 41 - 17
src/assets/scss/main.scss

@@ -26,26 +26,13 @@ main {
   height: 100vh;
   background-color: var(--color-light-bg);
 
-  button {
-    padding: 0.2rem 0.6rem;
-    font-size: 0.8rem;
-    margin: auto;
-    cursor: pointer;
-    background-color: var(--color-light-bg);
-    box-sizing: border-box;
-    border: 1px solid var(--color-border);
-    outline: 8px solid var(--color-light-bg);
-    color: var(--color-secondary);
-
-    &:focus {
-      outline: 8px solid var(--color-primary2);
-    }
-  }
   .carousel__next, .carousel__prev, .carousel__pagination-button {
     outline: none;
   }
 }
 
+//Custom set
+
 .main-bg {
   background-size: cover;
   background-position: center;
@@ -139,7 +126,7 @@ main {
   }
 
   &:focus {
-    outline: 4px solid var(--color-primary);
+    outline: 2px solid var(--color-primary);
   }
 }
 .main-any-button {
@@ -171,7 +158,6 @@ main {
     outline: 4px solid var(--color-primary);
   }
 }
-
 .main-map-maker {
   display: flex;
   flex-direction: column;
@@ -205,6 +191,8 @@ main {
   overflow: hidden;
 }
 
+//Html controls
+
 hr {
   border-color: var(--color-divider);
   border-width: 1px;
@@ -212,6 +200,42 @@ hr {
 h1, h2, h3, h4, h5 {
   margin-top: 0;
 }
+button {
+  padding: 0.4rem 0.8rem;
+  font-size: 0.8rem;
+  margin: auto;
+  cursor: pointer;
+  background-color: var(--color-light-bg);
+  box-sizing: border-box;
+  border: 1px solid var(--color-border);
+  outline: 4px solid var(--color-light-bg);
+  color: var(--color-secondary);
+
+  &:focus {
+    outline: 4px solid var(--color-primary);
+  }
+}
+form {
+  display: flex;
+  flex-direction: column;
+  align-items: stretch;
+  gap: 20px
+}
+input {
+  width: 100%;
+  padding: 0.4rem 0.8rem;
+  font-size: 0.8rem;
+  margin: auto;
+  cursor: pointer;
+  background-color: var(--color-light-bg);
+  box-sizing: border-box;
+  border: 1px solid var(--color-border);
+  color: var(--color-secondary);
+
+  &:focus {
+    border-color: var(--color-primary);
+  }
+}
 
 
 

+ 1 - 0
src/main.ts

@@ -2,6 +2,7 @@ import './assets/scss/main.scss'
 import './assets/scss/mengyuu/index.scss'
 import "@vuemap/vue-amap/dist/style.css";
 import '@imengyu/vue-scroll-rect/lib/vue-scroll-rect.css';
+import '@imengyu/vue-dynamic-form/dist/style.css';
 import "vue3-carousel/carousel.css";
 import 'sweetalert2/src/sweetalert2.scss';
 

+ 3 - 3
src/views/ContentView.vue

@@ -93,9 +93,9 @@ watch(() => activeTab.value, (newVal) => {
         <TabCommonList 
           v-else-if="activeTabId === 3" 
           :modelId="16" 
-          :mainBodyColumnId="[ [189,190,191,291],187,{ modelId: 5, mainBodyColumnId: [] } ]" 
-          :mainBodyColumnAsTabs="[ '闽南童谣', '讲古', '方言' ]"
-          :detailPageNames="['VideoDetail', 'VideoDetail', 'Play']"
+          :mainBodyColumnId="[ 189, [191, 291], 190,187,{ modelId: 5, mainBodyColumnId: [] } ]" 
+          :mainBodyColumnAsTabs="[ '闽南童谣', '闽南语经典歌曲', '南音', '讲古', '方言' ]"
+          :detailPageNames="['VideoDetail', 'VideoDetail', 'VideoDetail', 'VideoDetail', 'Play']"
         />
         <TabCustomList v-else-if="activeTabId === 4" />
         <TabInherit v-else-if="activeTabId === 5" />

+ 15 - 11
src/views/Details/CommonDetail.vue

@@ -37,9 +37,8 @@ watch(route, () => loader.loadData(undefined, true));
 <template>
   <main class="main-content main-bg main-bg1">
     <Header show-back absolute :custom-back="customBack" @back="emit('back')" />
-
-    <SimplePageContentLoader :loader="loader">
-      <div v-if="loader.content.value" class="content absolute">
+    <div class="content absolute">
+      <SimplePageContentLoader :loader="loader">
         <div class="left" :style="{ backgroundImage: `url(${loader.content.value?.image})` }" />
         <div class="right">
           <h1>{{ loader.content.value?.title }}</h1>
@@ -61,8 +60,8 @@ watch(route, () => loader.loadData(undefined, true));
             @moreClick="showPreview = true; activeItem=5"
           />
         </div>
-      </div>
-    </SimplePageContentLoader>
+      </SimplePageContentLoader>
+    </div>
     <ImagePreview 
       v-model:active-item="activeItem"
       v-model:show="showPreview"
@@ -76,6 +75,11 @@ watch(route, () => loader.loadData(undefined, true));
   overflow: hidden;
   width: 100%;
   max-height: 100vh;
+
+  :deep(.error) {
+    padding-top: 200px;
+    height: 100%;
+  }
 }
 .left {
   position: absolute;
@@ -89,14 +93,16 @@ watch(route, () => loader.loadData(undefined, true));
 .right {
   position: absolute;
   top: 0;
+  bottom: 0;
   right: 0;
   width: 60vw;
-  height: 80%;
   z-index: 1;
   background: linear-gradient(90deg, rgba(240, 235, 222, 0.1) 0%, #f0ebde 15%, #f0ebde 100%);
   font-size: 0.8rem;
   color: var(--color-text);
-  padding: 6% 8%;
+  padding: 0 8%;
+  padding-top: 100px;
+  padding-bottom: 6%;
   padding-left: 20%;
   display: flex;
   flex-direction: column;
@@ -134,16 +140,14 @@ watch(route, () => loader.loadData(undefined, true));
 
 @media (max-height: 800px) {
   .right {
-    padding-top: 4%;
+    padding-top: 100px;
     padding-bottom: 4%;
-    height: 88%;
   }
 }
 @media (max-height: 400px) {
   .right {
-    padding-top: 2%;
+    padding-top: 100px;
     padding-bottom: 2%;
-    height: 86%;
   }
 }
 

+ 204 - 49
src/views/HomeView.vue

@@ -12,11 +12,60 @@ import Card5 from '@/assets/images/Home/Card/5.jpg';
 import Card6 from '@/assets/images/Home/Card/6.jpg';
 import Card7 from '@/assets/images/Home/Card/7.jpg';
 import Card8 from '@/assets/images/Home/Card/8.jpg';
-import { onMounted } from 'vue';
+import { computed, onMounted, ref } from 'vue';
+import SimplePopup from '@/components/SimplePopup.vue';
+import { DynamicForm, type IDynamicFormOptions, type IDynamicFormRef } from '@imengyu/vue-dynamic-form';
+import IndexContent from '@/api/introduction/IndexContent';
+import { SettingsUtils } from '@imengyu/imengyu-utils';
+import Tab from '@/components/small/Tab.vue';
 
 const router = useRouter();
-const mainSwiperData = useSimpleDataLoader(async () => {
-  return [
+const defaultCards = [Card1, Card2, Card3, Card4, Card5, Card6, Card7, Card8];
+const mainPageDataDefault = {
+  title: 'Default',
+  buttons: [
+    { 
+      image: Card1,
+      title: '保护概况',
+      id: 1,
+    },
+    { 
+      image: Card2,
+      title: '闽南精神',
+      id: 2,
+      },
+    { 
+      image: Card3,
+      title: '古早话仙',
+      id: 3,
+    },
+    { 
+      image: Card4,
+      title: '民俗风情',
+      id: 4,
+      },
+    { 
+      image: Card5,
+      title: '保护传承',
+      id: 5,
+    },
+    { 
+      image: Card6,
+      title: '闽南时尚',
+      id: 6,
+    },
+    { 
+      image: Card7,
+      title: '文化之旅',
+      id: 7,
+    },
+    { 
+      image: Card8,
+      title: '厝边记忆',
+      id: 8,
+    },
+  ],
+  swiper: [
     {
       image: Swiper1,
       title: ''
@@ -25,83 +74,162 @@ const mainSwiperData = useSimpleDataLoader(async () => {
       image: Swiper2,
       title: ''
     },
-  ];
+  ],
+};
+const mainPageHasCustomConfig = ref(false);
+const mainPageCurrentShowConfig = ref(false);
+const mainPageCustomConfigData = useSimpleDataLoader(async () => {
+  const deviceCode = SettingsUtils.getSettings('deviceCode', '');
+  if (deviceCode) {
+    const config = await IndexContent.getTerminalConfig(deviceCode);
+    mainPageHasCustomConfig.value = true;
+    mainPageCurrentShowConfig.value = true;
+    return {
+      title: config.name,
+      swiper: config.images.map((item) => ({
+        image: item,
+        title: ''
+      })),
+      buttons: config.pageJson.map((item, i) => ({
+        image: item.logo || defaultCards[i],
+        title: item.title,
+        page: item.page,
+        id: i,
+      })),
+    };
+  } else return mainPageDataDefault;
+});
+const mainPageData = computed(() => {
+  if (mainPageHasCustomConfig.value && mainPageCurrentShowConfig.value) {
+    return mainPageCustomConfigData.content.value;
+  } else return mainPageDataDefault;
 });
-const mainCardData = [
-  { 
-    image: Card1,
-    title: '保护概况',
-    id: 1,
+
+
+const showBindDeviceCode = ref(false);
+const bindDeviceCodeFormModel = ref({
+  userName: '',
+  deviceCode: '',
+});
+const bindDeviceCodeFormStatus = ref('');
+const bindDeviceCodeFormRef = ref<IDynamicFormRef>();
+const bindDeviceCodeFormOptions = ref<IDynamicFormOptions>({
+  formLabelCol: { span: 0 },
+  formWrapperCol: { span: 24 },
+  formRules: {
+    userName: [
+      { required: true, message: '请输入用户名', trigger: 'blur' }
+    ],
+    deviceCode: [
+      { required: true, message: '请输入机器码', trigger: 'blur' }
+    ],
   },
-  { 
-    image: Card2,
-    title: '闽南精神',
-    id: 2,
+  formItems: [
+    {
+      type: 'base-text',
+      label: '',
+      name: 'userName',
+      additionalProps: { placeholder: '请输入用户名' }
     },
-  { 
-    image: Card3,
-    title: '古早话仙',
-    id: 3,
-  },
-  { 
-    image: Card4,
-    title: '民俗风情',
-    id: 4,
+    {
+      type: 'base-text',
+      label: '',
+      name: 'deviceCode',
+      additionalProps: { placeholder: '请输入机器码' }
     },
-  { 
-    image: Card5,
-    title: '保护传承',
-    id: 5,
-  },
-  { 
-    image: Card6,
-    title: '闽南时尚',
-    id: 6,
-  },
-  { 
-    image: Card7,
-    title: '文化之旅',
-    id: 7,
-  },
-  { 
-    image: Card8,
-    title: '厝边记忆',
-    id: 8,
-  },
-];
+  ],
+});
 
-function handleClick(id: number) {
-  router.push({ name: 'Content', query: { tab: id } });
+async function handleBindDeviceCode() {
+  bindDeviceCodeFormStatus.value = '设置中请稍等...';
+  try {
+    await IndexContent.bindMachineCode(bindDeviceCodeFormModel.value.userName, bindDeviceCodeFormModel.value.deviceCode);
+    bindDeviceCodeFormStatus.value = '绑定成功,正在刷新请稍等...';
+    SettingsUtils.setSettings('deviceCode', bindDeviceCodeFormModel.value.deviceCode);
+    setTimeout(() => {
+      mainPageCustomConfigData.loadData(undefined, true);
+      showBindDeviceCode.value = false;
+    }, 1000);
+  } catch (e) {
+    bindDeviceCodeFormStatus.value = '绑定失败,' + e;
+  }
 }
+function handleClick(item: any) {
+  if (item.page) {
+    const p = item.page.split('?');
+    const query : Record<string, any> = {};
+    p[1].split('&').forEach((item: string) => {
+      const j = item.split('=');
+      query[j[0]] = j[1];
+    });
+    router.push({ name: p[0], query });
+  } else
+    router.push({ name: 'Content', query: { tab: item.id } });
+}
+
+/* onMounted(() => {
+  SettingsUtils.setSettings('deviceCode', '1122');
+}) */
 </script>
 
 <template>
   <main class="main-content">
     <div class="main-bg absolute">
-      <ImageSwiper :items="mainSwiperData.content.value ?? []" :autoplay="4000" :showAddons="false" :tabindex="-1">
+      <ImageSwiper :items="mainPageData?.swiper ?? []" :autoplay="4000" :showAddons="false" :tabindex="-1">
         <template #item="{ item }">
           <img :src="item.image" alt="">
         </template>
       </ImageSwiper>
     </div>
     <div class="content absolute d-flex flex-col">
-      <img src="@/assets/images/Home/title.png" alt="" class="main-title">
+      <div class="invisible-button">
+        <div v-if="mainPageHasCustomConfig" class="d-flex flex-row">
+          <Tab :modelValue="mainPageCurrentShowConfig? 1 : 0" :tabs="[
+            { label: '闽南文化' },
+            { label: mainPageCustomConfigData.content.value?.title || '自定义' },
+          ]" @update:modelValue="(v) => mainPageCurrentShowConfig = v == 1" />
+        </div>
+        <div v-else class="main-any-button box" :tabindex="1" @click="showBindDeviceCode=true" />
+      </div>
+
+      <img v-if="mainPageData?.title === 'Default'" src="@/assets/images/Home/title.png" alt="" class="main-title">
+      <div v-else class="main-title">{{ mainPageData?.title }}</div>
+
       <div class="main-card-space" />
       <div class="main-card-list">
         <div 
-          v-for="(item, index) in mainCardData"
+          v-for="(item, index) in mainPageData?.buttons ?? []"
           :key="index"
           :tabindex="1"
           :style="{
             backgroundImage: `url(${item.image})`
           }"
           class="main-card round"
-          @click="handleClick(item.id)"
+          @click="handleClick(item)"
         >
           {{ item.title }}
         </div>
       </div>
     </div>
+    <SimplePopup 
+      :show="showBindDeviceCode"
+      @update:show="(v) => showBindDeviceCode = v"
+    >
+      <div class="bind-device">
+        <h1 class="title">绑定机器码</h1>
+        <DynamicForm 
+          ref="bindDeviceCodeFormRef"
+          :model="bindDeviceCodeFormModel"
+          :options="bindDeviceCodeFormOptions" 
+          @submit="handleBindDeviceCode"
+        />
+        <p v-if="bindDeviceCodeFormStatus" class="text-danger">{{ bindDeviceCodeFormStatus }}</p>
+        <div class="d-flex flex-row p-3 pl-0 pr-0 justify-between gap-ss">
+          <button class="flex-one" @click="showBindDeviceCode=false">取消</button>
+          <button class="flex-one" @click="bindDeviceCodeFormRef?.submit()">确定</button>
+        </div>
+      </div>
+    </SimplePopup>
   </main>
 </template>
 
@@ -114,10 +242,24 @@ function handleClick(id: number) {
 }
 .main-title {
   width: 40%;
+  color: #fff;
+  font-size: 3rem;
 }
 .main-card-space {
   height: 50%;
 }
+.bind-device  {
+  background-color: #fff;
+  border-radius: 1rem;
+  padding: 1.5rem;
+  width: 400px;
+
+  h1 {
+    font-size: 1rem;
+    margin-bottom: 2rem;
+    text-align: center;
+  }
+}
 
 .main-card-list {
   position: relative;
@@ -125,6 +267,7 @@ function handleClick(id: number) {
   min-height: 30vh;
   display: grid;
   grid-template-columns: repeat(4, 1fr);
+  grid-template-rows: repeat(2, 1fr);
   grid-gap: 20px;
   font-size: 26px;
 
@@ -134,6 +277,18 @@ function handleClick(id: number) {
     color: var(--color-primary);
   }
 }
+.invisible-button {
+  position: absolute;
+  right: 1rem;
+  top: 1rem;
+  min-width: 3rem;
+  height: 3rem;
+
+  .box {
+    width: 3rem;
+    height: 3rem;
+  }
+}
 
 @media (max-width: 800px) {
   .content {

+ 3 - 2
src/views/Intangible/village/detail.vue

@@ -16,7 +16,8 @@
             class="main-round-box flat"
             :items="data.images"
             :autoplay="2500"
-            style="height:300px"
+            :showAddons="false"
+            style="height:20vh"
           >
             <template #item="{ item }">
               <img class="swiper-slide" :src="item" />
@@ -195,7 +196,7 @@ async function loadInfo() {
   .map-container {
     position: relative;
     width: 100%;
-    height: 250px;
+    height: 20vh;
     margin: 40px 0;
     overflow: hidden;
     border-radius: 8px;