快乐的梦鱼 před 1 měsícem
rodič
revize
7b22ed8c2b
100 změnil soubory, kde provedl 8046 přidání a 6247 odebrání
  1. 1 0
      .gitignore
  2. 9 0
      config.json.example
  3. 14 21
      nuxt.config.ts
  4. 6410 5746
      package-lock.json
  5. 6 2
      package.json
  6. 23 0
      server/api/channel/nav.ts
  7. 22 0
      server/api/health.ts
  8. 48 0
      server/config/db.ts
  9. 130 0
      server/db/CommonModel.ts
  10. 96 0
      server/db/DB.ts
  11. 109 0
      server/db/DBUtils.ts
  12. 73 0
      server/db/Query.ts
  13. 1071 0
      server/db/QueryGenerator.ts
  14. 27 0
      server/utils/response.ts
  15. 5 10
      src/App.vue
  16. 0 417
      src/api/CommonContent.ts
  17. 2 18
      src/api/RequestModules.ts
  18. 0 24
      src/api/Test.ts
  19. binární
      src/assets/fonts/Impact.ttf
  20. binární
      src/assets/fonts/Impact.woff
  21. binární
      src/assets/fonts/Impact.woff2
  22. binární
      src/assets/fonts/SourceHanSerifCN-Bold.otf
  23. binární
      src/assets/fonts/SourceHanSerifCN-Bold.ttf
  24. binární
      src/assets/fonts/SourceHanSerifCN-Bold.woff
  25. binární
      src/assets/fonts/SourceHanSerifCN-Bold.woff2
  26. binární
      src/assets/fonts/nzgrRuyinZouZhangKai.ttf
  27. binární
      src/assets/fonts/nzgrRuyinZouZhangKai.woff
  28. binární
      src/assets/fonts/nzgrRuyinZouZhangKai.woff2
  29. 0 1
      src/assets/images/404.svg
  30. binární
      src/assets/images/BackArrow.png
  31. binární
      src/assets/images/Bg1.jpg
  32. binární
      src/assets/images/Bg2.jpg
  33. binární
      src/assets/images/BgLong.jpg
  34. binární
      src/assets/images/CloseMini.png
  35. binární
      src/assets/images/DropDownArrow.png
  36. binární
      src/assets/images/Icon@2x.png
  37. binární
      src/assets/images/IconArrowRight.png
  38. 0 8
      src/assets/images/IconInfo.svg
  39. binární
      src/assets/images/ImageFailed.png
  40. binární
      src/assets/images/LargeTitle1.png
  41. binární
      src/assets/images/LargeTitle2.png
  42. binární
      src/assets/images/LargeTitle3.png
  43. binární
      src/assets/images/Logo2.png
  44. binární
      src/assets/images/LogoIcon.png
  45. binární
      src/assets/images/LogoIconDark.png
  46. binární
      src/assets/images/TitleMiniHeader.png
  47. binární
      src/assets/images/about-logo.png
  48. binární
      src/assets/images/about/Banner.jpg
  49. binární
      src/assets/images/about/IconLocation.png
  50. binární
      src/assets/images/about/IconMail.png
  51. binární
      src/assets/images/about/IconMobile.png
  52. binární
      src/assets/images/about/Logo.png
  53. binární
      src/assets/images/badge.png
  54. binární
      src/assets/images/box-service.png
  55. binární
      src/assets/images/button-active.png
  56. binární
      src/assets/images/button-deactive.png
  57. binární
      src/assets/images/communicate/Banner.jpg
  58. binární
      src/assets/images/communicate/Image1.jpg
  59. binární
      src/assets/images/communicate/Image2.jpg
  60. binární
      src/assets/images/communicate/Image3.jpg
  61. binární
      src/assets/images/favicon.ico
  62. binární
      src/assets/images/favicon.png
  63. binární
      src/assets/images/footer-bg.png
  64. binární
      src/assets/images/footer/FooterPrinting.png
  65. binární
      src/assets/images/footer/GonganLogo.png
  66. binární
      src/assets/images/fusion/Banner.jpg
  67. binární
      src/assets/images/fusion/Image1.jpg
  68. binární
      src/assets/images/fusion/Image2.jpg
  69. binární
      src/assets/images/fusion/Image3.jpg
  70. binární
      src/assets/images/fusion/Image4.jpg
  71. binární
      src/assets/images/fusion/Image5.jpg
  72. binární
      src/assets/images/fusion/Image6.jpg
  73. binární
      src/assets/images/icon-contract.png
  74. binární
      src/assets/images/icon-explore.png
  75. binární
      src/assets/images/icon-join.png
  76. binární
      src/assets/images/index/Box1.png
  77. binární
      src/assets/images/index/Box2.jpg
  78. binární
      src/assets/images/index/Box3.jpg
  79. binární
      src/assets/images/index/BoxPrinting1.png
  80. binární
      src/assets/images/index/BoxPrinting2.png
  81. binární
      src/assets/images/index/BoxPrinting4.png
  82. binární
      src/assets/images/index/ButtonMore.png
  83. binární
      src/assets/images/index/Introd.jpg
  84. binární
      src/assets/images/index/IntrodLeft.png
  85. binární
      src/assets/images/index/IntrodRight.jpg
  86. binární
      src/assets/images/inheritor/Banner.jpg
  87. binární
      src/assets/images/inheritor/Image1.jpg
  88. binární
      src/assets/images/inheritor/Image10.jpg
  89. binární
      src/assets/images/inheritor/Image11.jpg
  90. binární
      src/assets/images/inheritor/Image2.jpg
  91. binární
      src/assets/images/inheritor/Image3.jpg
  92. binární
      src/assets/images/inheritor/Image4.jpg
  93. binární
      src/assets/images/inheritor/Image5.jpg
  94. binární
      src/assets/images/inheritor/Image6.jpg
  95. binární
      src/assets/images/inheritor/Image7.jpg
  96. binární
      src/assets/images/inheritor/Image8.jpg
  97. binární
      src/assets/images/inheritor/Image9.jpg
  98. binární
      src/assets/images/inheritor/LawsTest.jpg
  99. binární
      src/assets/images/inheritor/SubmitButton.png
  100. 0 0
      src/assets/images/introduction/Banner.jpg

+ 1 - 0
.gitignore

@@ -6,6 +6,7 @@ yarn-debug.log*
 yarn-error.log*
 pnpm-debug.log*
 lerna-debug.log*
+/config.json
 
 node_modules
 .DS_Store

+ 9 - 0
config.json.example

@@ -0,0 +1,9 @@
+{
+  "db": {
+    "host": "localhost",
+    "port": 3306,
+    "user": "root",
+    "password": "your_password",
+    "database": "wenbao"
+  }
+}

+ 14 - 21
nuxt.config.ts

@@ -1,9 +1,11 @@
+import { defineNuxtConfig } from "nuxt/config";
+
 // https://nuxt.com/docs/api/configuration/nuxt-config
 export default defineNuxtConfig({
   compatibilityDate: '2025-05-15',
   app: {
     head: {
-      title: '闽南文化生态保护区(厦门市)',
+      title: '厦门市文化遗产保护中心',
       viewport: 'width=device-width, initial-scale=1, maximum-scale=1',
       htmlAttrs: {
         lang: 'zh',
@@ -12,10 +14,11 @@ export default defineNuxtConfig({
         { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
       ]
     }
-  },
+  }, 
+  css: ['bootstrap/dist/css/bootstrap.min.css'],
   devtools: { enabled: true },
   srcDir: 'src/',
-  modules: ['@pinia/nuxt', '@ant-design-vue/nuxt'],
+  modules: ['@pinia/nuxt', '@ant-design-vue/nuxt', '@nuxt/icon'],
   components: [
     {
       path: '~/components',
@@ -23,6 +26,10 @@ export default defineNuxtConfig({
       extensions: ['.vue'],
     }
   ],
+  icon: {
+    provider: 'iconify',
+    serverBundle: false,
+  },
   build: {
     transpile: [
       '@imengyu/vue-scroll-rect',
@@ -41,27 +48,13 @@ export default defineNuxtConfig({
     },
   },
   routeRules: {
-    //'/**': { swr: false, isr: false, headers: { 'cache-control': 'no-store, max-age=0' } },
+    '/**': { swr: false, isr: false, headers: { 'cache-control': 'no-store, max-age=0' } },
     
-    /**/
+    /*
     '/': { swr: 1800 },
+    */
     '/about/': { swr: 1800 },
-    '/communicate/': { swr: 1800 },
-    '/fusion/': { swr: 1800 },
-    '/inheritor/': { swr: 1800 },
-    '/introduction/': { swr: 1800 },
-    '/news/': { swr: 1800 },
-    '/research/': { swr: 1800 },
-
     '/introduction/**': { swr: true },
-    '/communicate/**': { swr: true },
-    '/fusion/**': { swr: true },
-    '/inheritor/**': { swr: true },
-    '/news/**': { swr: true },
-    '/research/**': { swr: true },
-    '/village/**': { swr: true },
-    
-
-    '/inheritor/submit': { ssr: false },
+    '/inheritor/submit': { swr: false },
   }
 })

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 6410 - 5746
package-lock.json


+ 6 - 2
package.json

@@ -19,15 +19,18 @@
     "@imengyu/js-request-transform": "^0.3.5",
     "@imengyu/vue-dynamic-form": "^0.1.1",
     "@imengyu/vue-scroll-rect": "^0.1.3",
+    "@nuxt/icon": "^2.1.1",
     "@pinia/nuxt": "^0.11.1",
+    "@popperjs/core": "^2.11.8",
     "@vuemap/vue-amap": "^2.1.12",
     "ant-design-vue": "^4.2.6",
     "axios": "^1.9.0",
-    "bootstrap": "^5.3.0",
+    "bootstrap": "^5.3.8",
     "lodash-es": "^4.17.21",
     "md5": "^2.3.0",
     "mitt": "^3.0.1",
-    "nuxt": "^3.17.6",
+    "mysql2": "^3.16.0",
+    "nuxt": "^4.2.2",
     "pinia": "^3.0.1",
     "tslib": "^2.8.1",
     "vue": "^3.5.13",
@@ -35,6 +38,7 @@
     "vue3-carousel": "^0.15.0"
   },
   "devDependencies": {
+    "@iconify-json/material-symbols": "^1.2.50",
     "@inquirer/prompts": "^7.5.3",
     "@tsconfig/node22": "^22.0.1",
     "@types/node": "^22.14.0",

+ 23 - 0
server/api/channel/nav.ts

@@ -0,0 +1,23 @@
+import { defineEventHandler, EventHandlerRequest } from 'h3';
+import { DB } from '~~/server/db/DB';
+import { createErrorResponse, createSuccessResponse, IResponse } from '~~/server/utils/response';
+
+export default defineEventHandler<EventHandlerRequest, Promise<IResponse<{
+  id: number;
+  model_id: number;
+  name: string;
+  type: 'list'|'link',
+  url: string;
+  outlink: string;
+  diyname: string;
+}[]>>>(async (event) => {
+  try {
+    return createSuccessResponse(await DB.table('pr_cms_channel')
+            .where('status', 'normal')
+            .where('isnav', 1)
+            .select('*')
+            .get());
+  } catch (error) {
+    return createErrorResponse(error);
+  }
+});

+ 22 - 0
server/api/health.ts

@@ -0,0 +1,22 @@
+import { defineEventHandler } from 'h3';
+import { testConnection } from '../config/db';
+
+export default defineEventHandler(async (event) => {
+  try {
+    // 测试数据库连接
+    await testConnection();
+    
+    return {
+      status: 'ok',
+      timestamp: new Date().toISOString(),
+      message: '✅ 服务运行正常,数据库连接成功'
+    };
+  } catch (error) {
+    return {
+      status: 'error',
+      timestamp: new Date().toISOString(),
+      message: '❌ 服务运行异常',
+      error: error instanceof Error ? error.message : String(error)
+    };
+  }
+});

+ 48 - 0
server/config/db.ts

@@ -0,0 +1,48 @@
+import mysql from 'mysql2/promise';
+import { readFileSync } from 'fs';
+import { join } from 'path';
+
+// 尝试从config.json文件读取配置
+let jsonConfig: any = {};
+
+try {
+  // 从项目根目录读取config.json文件
+  const configPath = join(process.cwd(), 'config.json');
+  const configContent = readFileSync(configPath, 'utf-8');
+  jsonConfig = JSON.parse(configContent);
+} catch (error) {
+  console.log('⚠️ 无法读取config.json,将使用环境变量或默认值');
+}
+
+// 数据库配置
+const config = {
+  host: process.env.DB_HOST || jsonConfig.db?.host || 'localhost',
+  port: Number(process.env.DB_PORT) || jsonConfig.db?.port || 3306,
+  user: process.env.DB_USER || jsonConfig.db?.user || 'root',
+  password: process.env.DB_PASSWORD || jsonConfig.db?.password || '',
+  database: process.env.DB_NAME || jsonConfig.db?.database || 'protection_center',
+  waitForConnections: true,
+  connectionLimit: 10,
+  queueLimit: 0
+};
+
+// 创建连接池
+const pool = mysql.createPool(config);
+
+// 测试数据库连接
+async function testConnection() {
+  try {
+    const connection = await pool.getConnection();
+    console.log('✅ 数据库连接成功');
+    connection.release();
+  } catch (error) {
+    console.error('❌ 数据库连接失败:', error);
+    throw error;
+  }
+}
+
+export {
+  pool,
+  testConnection,
+  config
+};

+ 130 - 0
server/db/CommonModel.ts

@@ -0,0 +1,130 @@
+import { type ConvertItemOptions, type ConvertTable, DataConverter } from "@imengyu/js-request-transform";
+
+interface CommonModelConfig {
+  idKey?: string;
+  convertTable?: ConvertTable;
+}
+
+export interface NewCommonModel<T extends CommonModel> {
+  new(): T;
+}
+export type ValidCommonModel<T> = T extends CommonModel ? NewCommonModel<T> : undefined;
+
+export class CommonModel {
+  public _tableName: string;
+  public _config: CommonModelConfig;
+
+  public constructor(tableName: string, config: CommonModelConfig) { 
+    this._tableName = tableName;
+    this._config = config;
+    this._config.convertTable = {
+      ...this._config.convertTable,
+      createdAt: { clientSide: 'date', serverSide: 'string' },
+      updatedAt: { clientSide: 'date', serverSide: 'string' },
+      deletedAt: { clientSide: 'date', serverSide: 'string' },
+    }
+  }
+
+  createdAt = new Date();
+  updatedAt : Date | null = null;
+  deletedAt : Date | null = null;
+
+  public setValue(key: keyof this, value: any) {
+    (this as any)[key] = value;
+    return this;
+  }
+  public setValues(at: {
+    [key in keyof this]?: any;
+  }) {
+    for (const key in at)
+      this.setValue(key, at[key]);
+    return this;
+  }
+
+  public fromServerSide(data: any) {
+    const self = this as Record<string, any>;
+    const options : ConvertItemOptions = {
+      direction: 'client',
+      defaultDateFormat: 'YYYY-MM-DD HH:mm:ss',
+      policy: 'default',
+    }
+    for (const key in data) {
+      if (key.startsWith('_'))
+        continue;
+      const convert = this._config.convertTable?.[key];
+      if (convert) {
+        let value = data[key];
+        if (convert instanceof Array)
+          for (const convertItem of convert) 
+            value = DataConverter.convertDataItem(value, key, convertItem, options, `${key}`, this._tableName);
+        else
+          value = DataConverter.convertDataItem(value, key, convert, options, `${key}`, this._tableName)
+        self[key] = value;
+      } else {
+        self[key] = data[key];
+      }
+    }
+    return this;
+  }
+  public toServerSide() {
+    const self = this as Record<string, any>;
+    const options : ConvertItemOptions = {
+      direction: 'server',
+      defaultDateFormat: 'YYYY-MM-DD HH:mm:ss',
+      policy: 'default',
+    }
+    const result : Record<string, any> = {};
+    for (const key in self) {
+      if (key.startsWith('_'))
+        continue;
+      const convert = this._config.convertTable?.[key];
+      if (convert) {
+        let value = self[key];
+        if (convert instanceof Array)
+          for (const convertItem of convert)
+            value = DataConverter.convertDataItem(value, key, convertItem, options, `${key}`, this._tableName);
+        else
+          value = DataConverter.convertDataItem(value, key, convert, options, `${key}`, this._tableName);
+        result[key] = value;
+      } else {
+        result[key] = self[key];
+      }
+    }
+    return result;
+  }
+}
+
+/**
+ * 通用分页返回
+ */
+export class CommonPageResult<T> implements ICommonPageResult<T> {
+  public items : T[];
+  public pageIndex : number;
+  public pageSize : number;
+  public allCount : number;
+  public allPage : number;
+  public empty : boolean;
+
+  public constructor(model: ValidCommonModel<T>|undefined, data : any[], pageIndex : number, pageSize : number, allCount : number) {
+    this.items = data.map((item) => {
+      return model ? new model().fromServerSide(item) as T : item as T;
+    });
+    this.pageIndex = pageIndex;
+    this.pageSize = pageSize;
+    this.allCount = allCount;
+    this.allPage = Math.floor(allCount / pageSize) || 0;
+    this.empty = data.length == 0;
+  }
+}
+
+/**
+ * 通用分页返回结构
+ */
+export interface ICommonPageResult<T> {
+  items : T[];
+  pageIndex : number;
+  pageSize : number;
+  allCount : number;
+  allPage : number;
+  empty : boolean;
+}

+ 96 - 0
server/db/DB.ts

@@ -0,0 +1,96 @@
+import { CommonModel, NewCommonModel } from './CommonModel';
+import { solveSqlPlaceholders, splicingSQL, SqlStatcParams } from './DBUtils'
+import { query, queryAndReturnInsertId } from './Query';
+import { QueryGenerator } from './QueryGenerator';
+
+/**
+ * 数据库操作工具类 (SQLITE)
+ * 如何使用:
+ * DB.table('表名') 构造一个查询器
+ * DB.select/update/insert/delete 进行SQL查询,其中:
+ *      SQL中参数使用: {?} {参数索引} 来代表参数占位符,参数在args中传入
+ */
+export class DB {
+
+  /**
+   * 构造查询器
+   * @param name 当前表名
+   * @returns 返回构造查询器
+   */
+  static table<T extends CommonModel = any>(name : string|NewCommonModel<T>) {
+    return new QueryGenerator<T>(name);
+  }
+
+  /**
+   * 进行 select 查询
+   * @param sql SQL语句
+   * @param args 参数数组
+   * @param staticParams 静态参数
+   * @returns promise 成功则返回查询到的数组,错误则返回错误信息
+   */
+  static async select<T>(sql : string, args : unknown[], staticParams : SqlStatcParams = {}) {
+    let placeholders = solveSqlPlaceholders(sql);
+    let targetSql = splicingSQL(sql, placeholders, args, null, staticParams);
+    return await query(targetSql);
+  }
+  /**
+   * 执行 update 查询
+   * @param sql SQL语句
+   * @param args 参数数组
+   * @returns promise 成功则返回受影响的行数,错误则返回错误信息
+   */
+  static async update(sql : string, args : unknown[]) {
+    let placeholders = solveSqlPlaceholders(sql);
+    let targetSql = splicingSQL(sql, placeholders, args, null, {});
+    return await query(targetSql);
+  }
+  /**
+   * 执行 insert 查询
+   * @param sql SQL语句
+   * @param args 参数数组
+   * @returns promise 成功则返回新插入的ID,错误则返回错误信息
+   */
+  static async insert(sql : string, args : unknown[]) {
+    let placeholders = solveSqlPlaceholders(sql);
+    let targetSql = splicingSQL(sql, placeholders, args, null, {});
+    return await queryAndReturnInsertId(targetSql);
+  }
+  /**
+   * 执行 delete 查询
+   * @param sql SQL语句
+   * @param args 参数数组
+   * @returns promise 成功则返回受影响的行数,错误则返回错误信息
+   */
+  static async delete(sql : string, args : unknown[]) {
+    let placeholders = solveSqlPlaceholders(sql);
+    let targetSql = splicingSQL(sql, placeholders, args, null, {});
+    return await query(targetSql);
+  }
+
+  /**
+   * 手动开始事务
+   */
+  static beginTransaction() {
+    return this.actionSql('BEGIN');
+  }
+  /**
+   * 回滚事务
+   */
+  static rollBack() {
+    return this.actionSql('ROLLBACK');
+  }
+  /**
+   * 提交事务
+   */
+  static commit() {
+    return this.actionSql('COMMIT');
+  }
+  /**
+   * 异步执行sql
+   * @param sql 
+   * @returns 
+   */
+  static async actionSql(sql : string) {
+    return await query(sql);
+  }
+}

+ 109 - 0
server/db/DBUtils.ts

@@ -0,0 +1,109 @@
+import { StringUtils } from "@imengyu/imengyu-utils";
+import { DataModel } from "@imengyu/js-request-transform";
+
+export interface SqlPlaceholder {
+  index: number,
+  length: number,
+  name: string
+}
+
+/**
+ * SQL静态参数的结构
+ */
+export interface SqlStatcParams {
+  [index: string]: any;
+}
+
+const sqlPlaceholderCache = new Map<string, SqlPlaceholder[]>();
+
+/**
+ * 分割sql中的参数占位符
+ * @param sql 原始sql
+ * @returns 
+ */
+export function solveSqlPlaceholders(sql : string) {
+  const hash = StringUtils.stringHashCode(sql);
+
+  let result : SqlPlaceholder[]|null = null;
+
+  if (sqlPlaceholderCache.has(hash)) 
+    result = sqlPlaceholderCache.get(hash)!;
+  if (result)
+    return result;
+
+  result = [];
+
+  let currentBracketsStart = -1;
+  let currentBracketsEnd = -1;
+  let chr = '';
+
+  for(let i = 0; i < sql.length; i++) {
+    chr = sql.charAt(i);
+    if(currentBracketsStart < 0) {
+      if(chr === '{') currentBracketsStart = i;
+    } else {
+      if(chr === '}') {
+        currentBracketsEnd = i;
+        if(currentBracketsEnd - currentBracketsStart > 0) {
+          result.push({
+            index: currentBracketsStart,
+            length: currentBracketsEnd - currentBracketsStart + 1,
+            name: sql.substring(currentBracketsStart + 1, currentBracketsEnd)
+          });
+        } else { 
+          console.warn('Bad sql placeholder, the brackets dose not contains a name , at ' + currentBracketsStart);
+        }
+        currentBracketsEnd = -1;
+        currentBracketsStart = -1;
+      }
+    }
+
+    if(i === sql.length - 1 && currentBracketsStart >= 0) {
+      console.warn('Bad sql placeholder, not found end brackets, at ' + currentBracketsStart);
+    }
+  }
+
+  sqlPlaceholderCache.set(hash, result);
+  return result;
+}
+/**
+ * 按占位符和参数拼接最终sql
+ * @param sql 原始sql
+ * @param placeholders 占位符数据
+ * @param args 传入参数
+ * @returns 
+ */
+export function splicingSQL(sql : string, placeholders : SqlPlaceholder[], args: unknown[], fn : Function|null, staticParams : SqlStatcParams) {
+  let result = '';
+  let startOffset = 0;
+  let funcArgNames = (fn ? (fn.toString()
+    .replace(/((\/\/.*$)|(\/\*[\s\S]*?\*\/)|(\s))/mg,'')
+    .match(/^function\s*[^\(]*\(\s*([^\)]*)\)/m)?.[1]
+    .split(/,/)) : null) || null;
+
+  for (let i = 0; i < args.length; i++) {
+    const item = args[i];
+    if (item instanceof DataModel) 
+      args[i] = item.toServerSide();
+  }
+
+  for(let i = 0; i < placeholders.length; i++) {
+    let p = placeholders[i];
+    let index = p.name === '?' ? i : parseInt(p.name);
+    let argval = null;
+    if(index >= 0) argval = args[index];
+    else {
+      let ii = funcArgNames ? funcArgNames.indexOf(p.name) : -1;
+      argval = ii >= 0 ? args[ii] : staticParams[p.name];
+    }
+
+    result = result.concat(sql.substring(startOffset, p.index), (argval !== null ? argval : ''));
+    startOffset = p.index + p.length;
+
+    if(i === placeholders.length - 1) {
+      result = result.concat(sql.substring(startOffset));
+    }
+  }
+
+  return result;
+}

+ 73 - 0
server/db/Query.ts

@@ -0,0 +1,73 @@
+import { QueryResult } from 'mysql2';
+import { pool } from '../config/db';
+
+/**
+ * 执行SELECT查询
+ * @param sql SQL查询语句
+ * @param params 查询参数
+ * @returns 查询结果
+ */
+export async function query<T extends QueryResult = any>(sql: string, params: any[] = []): Promise<T[]> {
+  const [rows] = await pool.execute<T>(sql, params);
+  return rows as T[];
+}
+
+export async function queryAndReturnInsertId(sql: string, params: any[] = []) : Promise<number> {
+  const [rows] = await pool.execute(sql, params);
+  const [insertIdRows] = await query('SELECT LAST_INSERT_ID() AS id');
+  return insertIdRows.id;
+}
+
+/**
+ * 执行INSERT查询
+ * @param sql SQL插入语句
+ * @param params 查询参数
+ * @returns 插入结果,包含insertId
+ */
+export async function insert(sql: string, params: any[] = []): Promise<{ insertId: number }> {
+  const [result] = await pool.execute(sql, params);
+  return result as { insertId: number };
+}
+
+/**
+ * 执行UPDATE查询
+ * @param sql SQL更新语句
+ * @param params 查询参数
+ * @returns 更新结果,包含affectedRows
+ */
+export async function update(sql: string, params: any[] = []): Promise<{ affectedRows: number }> {
+  const [result] = await pool.execute(sql, params);
+  return result as { affectedRows: number };
+}
+
+/**
+ * 执行DELETE查询
+ * @param sql SQL删除语句
+ * @param params 查询参数
+ * @returns 删除结果,包含affectedRows
+ */
+export async function remove(sql: string, params: any[] = []): Promise<{ affectedRows: number }> {
+  const [result] = await pool.execute(sql, params);
+  return result as { affectedRows: number };
+}
+
+/**
+ * 执行事务
+ * @param callback 事务回调函数
+ * @returns 事务执行结果
+ */
+export async function transaction<T>(callback: (connection: any) => Promise<T>): Promise<T> {
+  const connection = await pool.getConnection();
+  
+  try {
+    await connection.beginTransaction();
+    const result = await callback(connection);
+    await connection.commit();
+    return result;
+  } catch (error) {
+    await connection.rollback();
+    throw error;
+  } finally {
+    connection.release();
+  }
+}

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1071 - 0
server/db/QueryGenerator.ts


+ 27 - 0
server/utils/response.ts

@@ -0,0 +1,27 @@
+export interface IResponse<T> {
+  status: boolean;
+  message: string;
+  data?: T;
+}
+
+export function createSuccessResponse<T>(data?: T, message?: string): IResponse<T> {
+  return {
+    status: true,
+    message: message || '成功',
+    data
+  };
+}
+export function createErrorResponse( error?: any, message?: string): IResponse<any> {
+  return {
+    status: false,
+    message: message || '错误',
+    data: error
+  };
+}
+export function createResponse<T>(status: boolean, message: string, data?: T): IResponse<T> {
+  return {
+    status,
+    message,
+    data
+  };
+}

+ 5 - 10
src/App.vue

@@ -18,21 +18,19 @@
 
 <script setup lang="ts">
 import { onMounted, watch } from 'vue';
-import { RouterView, useRoute } from 'vue-router'
-import NavBar from './components/NavBar.vue';
-import Footer from './components/Footer.vue';
+import { useRoute } from 'vue-router'
 import { useAuthStore } from './stores/auth';
-import zhCN from 'ant-design-vue/es/locale/zh_CN';
-import VueAMap, {initAMapApiLoader} from '@vuemap/vue-amap';
+import { initAMapApiLoader } from '@vuemap/vue-amap';
 import { registryConvert } from './common/ConvertRgeistry'
-import { registerAllFormComponents } from './components/dynamicf';
+import zhCN from 'ant-design-vue/es/locale/zh_CN';
+import NavBar from './components/NavBar.vue';
+import Footer from './components/Footer.vue';
 
 if (import.meta.client) {
   initAMapApiLoader({
     key: '212b86dc49a5116a34e945d31da7ad14',
     securityJsCode: '46cae8205a707cfaf5801abcc4301566',
   });
-  registerAllFormComponents();
 } 
 registryConvert();
 
@@ -55,9 +53,6 @@ watch(route, () => {
 </script>
 
 <style>
-@import "bootstrap/dist/css/bootstrap.css";
-@import "bootstrap/dist/css/bootstrap-grid.css";
-@import "bootstrap/dist/css/bootstrap-utilities.css";
 @import "./assets/scss/main.scss";
 @import "vue3-carousel/carousel.css";
 @import "@vuemap/vue-amap/dist/style.css";

+ 0 - 417
src/api/CommonContent.ts

@@ -1,417 +0,0 @@
-import { DataModel, transformArrayDataModel, type NewDataModel } from '@imengyu/js-request-transform';
-import { AppServerRequestModule } from './RequestModules';
-import type { QueryParams } from "@imengyu/imengyu-utils";
-import { transformSomeToArray } from './Utils';
-
-export class GetColumListParams extends DataModel<GetColumListParams> {
-  
-  public constructor() {
-    super(GetColumListParams);
-    this.setNameMapperCase('Camel', 'Snake');
-  }
-
-  setModelId(val: number) {
-    this.modelId = val;
-    return this;
-  }
-  setMainBodyColumnId(val: number) {
-    this.mainBodyColumnId = val;
-    return this;
-  }
-  setFlag(val: 'hot'|'recommend'|'top') {
-    this.flag = val;
-    return this; 
-  }
-  setSize(val: number) {
-    this.size = val;
-    return this; 
-  }
-
-  modelId?: number;
-  /**
-   * 	主体栏目id
-   */
-  mainBodyColumnId: number = 0;
-  /**
-   * 标志:hot=热门,recommend=推荐,top=置顶
-   */
-  flag ?: 'hot'|'recommend'|'top';
-  /**
-   * 内容数量,默认4
-   */
-  size = 4;
-}
-export class GetContentListParams extends DataModel<GetContentListParams> {
-  
-  public constructor() {
-    super(GetContentListParams);
-    this.setNameMapperCase('Camel', 'Snake');
-    this._convertTable = {
-      ids: {
-        customToServerFn: (val) => (val as number[]).join(','),
-        customToClientFn: (val) => (val as string).split(',').map((item) => parseInt(item)),
-      },
-    }
-  }
-
-
-  setMainBodyColumnId(val: number|number[]) {
-    this.mainBodyColumnId = val;
-    return this;
-  }
-  setFlag(val: 'hot'|'recommend'|'top') {
-    this.flag = val;
-    return this; 
-  }
-  setIds(val: number[]) {
-    this.ids = val;
-    return this; 
-  }
-  setType(val: 1|2|3|4) {
-    this.type = val;
-    return this;
-  }
-  setSize(val: number) {
-    this.size = val;
-    return this;
-  }
-  setKeywords(val: string) {
-    this.keywords = val;
-    return this; 
-  }
-  setModelId(val: number) {
-    this.modelId = val;
-    return this; 
-  }
-
-  static TYPE_ARTICLE = 1;
-  static TYPE_AUDIO = 2;
-  static TYPE_VIDEO = 3;
-  static TYPE_IMAGE = 4;
-
-  modelId ?: number;
-  /**
-   * 主体栏目id
-   */
-  mainBodyColumnId: number|number[] = 0;
-  /**
-   * 标志:hot=热门,recommend=推荐,top=置顶
-   */
-  flag ?: 'hot'|'recommend'|'top';
-  /**
-   * 内容id(逗号隔开)如:3 或者 1,2,3
-   */
-  ids?: number[];
-  /**
-   * 类型:1=文章,2=音频,3=视频,4=相册
-   */
-  type?: 1|2|3|4;
-  /**
-   * 内容数量,默认4
-   */
-  size = 4;
-  /**
-   * 关键字查询
-   */
-  keywords?: string;
-
-}
-
-export class GetColumContentList extends DataModel<GetColumContentList> {
-  constructor() {
-    super(GetColumContentList, "主体栏目列表");
-    this.setNameMapperCase('Camel', 'Snake');
-    this._convertTable = {
-      id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
-      name: { clientSide: 'string', serverSide: 'string', clientSideRequired: true },
-      content_list: { 
-        clientSide: 'array',
-        clientSideRequired: true,
-        clientSideChildDataModel: GetContentListItem,
-      },
-    }
-  }
-
-  name = '';
-  overview = '';
-}
-export class GetContentListItem extends DataModel<GetContentListItem> {
-  constructor() {
-    super(GetContentListItem, "内容列表");
-    this.setNameMapperCase('Camel', 'Snake');
-    this._convertTable = {
-      id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
-      mainBodyColumnId: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
-      title: { clientSide: 'string', serverSide: 'string', clientSideRequired: true },
-      isGuest: { clientSide: 'boolean', serverSide: 'number' },
-      isLogin: { clientSide: 'boolean', serverSide: 'number' },
-      isComment: { clientSide: 'boolean', serverSide: 'number' },
-      isLike: { clientSide: 'boolean', serverSide: 'number' },
-      isCollect: { clientSide: 'boolean', serverSide: 'number' },
-      latitude: { clientSide: 'number', serverSide: 'number' },
-      longitude: { clientSide: 'number', serverSide: 'number' },
-      publishAt: { clientSide: 'date', serverSide: 'string' },
-      flag: { clientSide: 'splitCommaArray', serverSide: 'commaArrayMerge' },
-      tags: { clientSide: 'splitCommaArray', serverSide: 'commaArrayMerge' },
-      keywords: { clientSide: 'splitCommaArray', serverSide: 'commaArrayMerge' },
-      type: { clientSide: 'number', serverSide: 'number' },
-    };
-    this._convertKeyType = (key, direction) => {
-      if (key.endsWith('Time') || key.endsWith('At'))
-        return {
-          clientSide: 'date',
-          serverSide: 'string',
-        };
-      return undefined;
-    };
-  }
-  id = 0;
-  mainBodyColumnId = 0;
-  latitude = 0;
-  longitude = 0;
-  mapX = '';
-  mapY = '';
-  from = '';
-  modelId = 0;
-  title = '!title';
-  typeText = '';
-  region = 0;
-  image = '';
-  thumbnail = '';
-  desc = '!desc';
-  content = '!content';
-  type = 0;
-  keywords ?: string[];
-  flag ?: string[];
-  tags ?: string[];
-  views = 0;
-  comments = 0;
-  likes = 0;
-  collects = 0;
-  dislikes = 0;
-  district = '';
-  publishAt = new Date();
-}
-export class GetContentDetailItem extends DataModel<GetContentDetailItem> {
-  constructor() {
-    super(GetContentDetailItem, "内容详情");
-    this.setNameMapperCase('Camel', 'Snake');
-    this._convertTable = {
-      id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
-      title: { clientSide: 'string', serverSide: 'string' },
-      content: { clientSide: 'string', serverSide: 'string' },
-      isGuest: { clientSide: 'boolean', serverSide: 'number' },
-      isLogin: { clientSide: 'boolean', serverSide: 'number' },
-      isComment: { clientSide: 'boolean', serverSide: 'number' },
-      isLike: { clientSide: 'boolean', serverSide: 'number' },
-      isCollect: { clientSide: 'boolean', serverSide: 'number' },
-      publishAt: { clientSide: 'date', serverSide: 'string' },
-      flag: { clientSide: 'splitCommaArray', serverSide: 'commaArrayMerge' },
-      tags: { clientSide: 'splitCommaArray', serverSide: 'commaArrayMerge' },
-      type: { clientSide: 'number', serverSide: 'number' },
-      ichSitesList: { clientSide: 'array', clientSideChildDataModel: GetContentDetailItem },
-      inheritorsList: { clientSide: 'array', clientSideChildDataModel: GetContentDetailItem },
-      otherLevel: { clientSide: 'array', clientSideChildDataModel: GetContentDetailItem },
-    }
-    this._convertKeyType = (key, direction) => {
-      if (key.endsWith('Time') || key.endsWith('At'))
-        return {
-          clientSide: 'date',
-          serverSide: 'string',
-        };
-      else if (key.endsWith('List')) {
-        return [
-          { clientSide: 'map', serverSide: 'original'},
-          { clientSide: 'array', clientSideChildDataModel: GetContentDetailItem, serverSide: 'original' },
-        ]
-      }
-      return undefined;
-    };
-    this._afterSolveServer = () => {
-      if (this.image === 'https://mncdn.wenlvti.net')
-        this.image = '';
-      if (!this.image && this.images && this.images && this.images.length > 0  ) {
-        this.image = this.images[0]
-      }
-      if ((!this.images || this.images.length == 0) && this.image && this.image != '') {
-        this.images = [ this.image ]
-      }
-      if (!this.images)
-        this.images = []
-      if (this.publishVideo)
-        this.video = this.publishVideo;
-    }
-  }
-
-  id = 0;
-  from = '';
-  modelId = 0;
-  type = 0;
-  title = '';
-  region = 0;
-  image = '';
-  images = [] as string[];
-  audio = '';
-  video = '';
-  desc = '';
-  flag ?: string[];
-  tags ?: string[];
-  publishVideo?: string;
-  views = 0;
-  comments = 0;
-  likes = 0;
-  collects = 0;
-  dislikes = 0;
-  isLogin = false;
-  isGuest = false;
-  isComment = false;
-  isLike = false;
-  isCollect = false;
-  content = '';
-  value = '';
-  intro = '';
-  publishAt = new Date();
-  associationMeList = [] as {
-    id: number,
-    title: string,
-    image: string,
-    thumbnail: string,
-  }[];
-  otherLevel : GetContentDetailItem[] = [];
-}
-
-export class CategoryListItem extends DataModel<CategoryListItem> {
-  constructor() {
-    super(CategoryListItem, "分类列表");
-    this.setNameMapperCase('Camel', 'Snake');
-    this._convertTable = {
-      id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
-      pid: { clientSide: 'number', serverSide: 'number' },
-      haschild: { clientSide: 'boolean', serverSide: 'number' },
-    }
-  }
-
-  id !: number;
-  pid !: number;
-  title = '';
-  status = 'normal';
-  weight = 0;
-  spacer = '';
-  haschild = false;
-  children?: CategoryListItem[];
-}
-
-export class CommonContentApi extends AppServerRequestModule<DataModel> {
-
-  constructor(modelId: number, debugName: string, mainBodyColumnId?: number|number[]) {
-    super();
-    this.modelId = modelId;
-    this.mainBodyColumnId = mainBodyColumnId;
-    this.debugName = debugName;
-  }
-
-  public mainBodyColumnId?: number|number[];
-  public modelId: number;
-  protected debugName: string;
-
-  /**
-   * 获取分类列表
-   * @param type 根级类型:1=区域、2=级别、3=文物类型、4=非遗类型、42=事件类型
-   * @param withself 是否返回包含自己:true=是,false=否 ,默认false
-   * @returns 
-   */
-  async getCategoryList(
-    type?: number,
-    withself?: boolean,
-  ) {
-    return (this.get('/content/category/getCategoryList', '获取分类列表', {
-      type,
-      is_tree: false,
-      withself,
-    }))
-      .then(res => transformArrayDataModel<CategoryListItem>(CategoryListItem, res.data2, `获取分类列表`, true))
-      .catch(e => { throw e });
-  }
-  /**
-   * 用于获取某一个分类需要用的子级
-   * @param pid 父级
-   * @returns 
-   */
-  async getCategoryChildList(pid?: number) {
-    return (this.get('/content/category/getCategoryOnlyChildList', '获取分类子级列表', {
-      pid,
-    }))
-      .then(res => transformArrayDataModel<CategoryListItem>(
-        CategoryListItem, 
-        transformSomeToArray(res.data2), 
-        `获取分类列表`, 
-        true
-      ))
-      .catch(e => { throw e });
-  }
-
-  private toStringArray(arr: number|number[]|undefined) {
-    if (typeof arr === 'undefined') 
-      return '';
-    return typeof arr === 'object' ? arr.join(',') : arr.toString();
-  }
-
-  /**
-   * 主体栏目列表
-   * @param params 参数 
-   * @param querys 额外参数
-   * @returns 
-   */
-  getColumList<T extends DataModel = GetColumContentList>(params: GetColumListParams, modelClassCreator: NewDataModel = GetColumContentList, querys?: QueryParams) {
-    return this.get('/content/content/getMainBodyColumnContentList', `${this.debugName} 主体栏目列表`, {
-      model_id: this.modelId,
-      ...params.toServerSide(),
-      ...querys,
-    })
-      .then(res => ({
-        list: transformArrayDataModel<T>(modelClassCreator, res.data2.list, `${this.debugName} 主体栏目列表`, true),
-        total: res.data2.total as number,
-      }))
-      .catch(e => { throw e });
-  }
-  /**
-   * 模型内容列表
-   * @param params 参数
-   * @param page 页码
-   * @param pageSize 页大小
-   * @param querys 额外参数
-   * @returns 
-   */
-  getContentList<T extends DataModel = GetContentListItem>(params: GetContentListParams, page: number, pageSize: number = 10, modelClassCreator: NewDataModel = GetContentListItem, querys?: QueryParams) {
-    return this.get('/content/content/getContentList', `${this.debugName} 模型内容列表`, {
-      ...params.toServerSide(),
-      ...querys,
-      model_id: params.modelId || this.modelId,
-      main_body_column_id: this.toStringArray(params.mainBodyColumnId || this.mainBodyColumnId),
-      page,
-      pageSize,
-    })
-      .then(res => ({ 
-        list: transformArrayDataModel<T>(modelClassCreator, res.data2.list, `${this.debugName} 模型内容列表`, true),
-        total: res.data2.total as number,
-      }))
-      .catch(e => { throw e });
-  }
-  /**
-   * 内容详情
-   * @param id id 
-   * @param querys 额外参数
-   * @returns 
-   */
-  getContentDetail<T extends DataModel = GetContentDetailItem>(id: number, modelId?: number, modelClassCreator: NewDataModel = GetContentDetailItem, querys?: QueryParams) {
-    return this.get('/content/content/getContentDetail', `${this.debugName} (${id}) 内容详情`, {
-      model_id: modelId ?? this.modelId,
-      id,
-      ...querys,
-    }, modelClassCreator)
-      .then(res => res.data as T)
-      .catch(e => { throw e });
-  }
-}
-
-export default new CommonContentApi(0, '默认通用内容');

+ 2 - 18
src/api/RequestModules.ts

@@ -19,7 +19,6 @@ import {
   appendGetUrlParams, 
   appendPostParams,
 } from "@imengyu/imengyu-utils";
-import { logError } from "@/components/error/ErrorReporterIs";
 import type { DataModel, KeyValue, NewDataModel } from "@imengyu/js-request-transform";
 import { useAuthStore } from "@/stores/auth";
 import { Modal } from "ant-design-vue";
@@ -33,7 +32,7 @@ const notReportMessages = [
 ] as RegExp[];
 function matchNotReportMessage(str: string) {
   for (let i = 0; i < notReportMessages.length; i++) {
-    if (notReportMessages[i].test(str))
+    if (notReportMessages[i]?.test(str))
       return true;
   }
   return false;
@@ -174,23 +173,8 @@ function responseErrReoprtInceptor<T extends DataModel>(instance: RequestCoreIns
 export function reportError<T extends DataModel>(instance: RequestCoreInstance<T>, response: RequestApiError | Error) {
   if (import.meta.env.DEV) {
     if (response instanceof RequestApiError) {
-      logError({
-        message: `请求错误 ${response.apiName} : ${response.errorMessage}`,
-        detail: response.toString() +
-          '\r\n请求接口:' + response.apiName +
-          '\r\n请求地址:' + response.apiUrl +
-          '\r\n请求参数:' + JSON.stringify(response.rawRequest) +
-          '\r\n返回参数:' + JSON.stringify(response.rawData) +
-          '\r\n状态码:' + response.code +
-          '\r\n信息:' + response.errorCodeMessage,
-        type: 'error',
-      });
+      
     } else {
-      logError({
-        message: '错误报告 代码错误',
-        detail: response?.stack || ('' + response),
-        type: 'error',
-      });
     }
   } else {    
     let errMsg = '';

+ 0 - 24
src/api/Test.ts

@@ -1,24 +0,0 @@
-import { DataModel } from '@imengyu/js-request-transform';
-import { DefaultRequestModule } from '../common/request';
-
-export class Test extends DataModel {
-  public constructor() {
-    super();
-    this._convertTable = {
-    };
-  }
-}
-
-export class TestApi extends DefaultRequestModule<Test> {
-
-  constructor() {
-    super();
-    this.config.modelClassCreator = Test;
-  }
-
-  getDataTest() {
-    return this.get('http://127.0.0.1', 'getDataTest');
-  }
-}
-
-export default new TestApi();

binární
src/assets/fonts/Impact.ttf


binární
src/assets/fonts/Impact.woff


binární
src/assets/fonts/Impact.woff2


binární
src/assets/fonts/SourceHanSerifCN-Bold.otf


binární
src/assets/fonts/SourceHanSerifCN-Bold.ttf


binární
src/assets/fonts/SourceHanSerifCN-Bold.woff


binární
src/assets/fonts/SourceHanSerifCN-Bold.woff2


binární
src/assets/fonts/nzgrRuyinZouZhangKai.ttf


binární
src/assets/fonts/nzgrRuyinZouZhangKai.woff


binární
src/assets/fonts/nzgrRuyinZouZhangKai.woff2


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 1
src/assets/images/404.svg


binární
src/assets/images/BackArrow.png


binární
src/assets/images/Bg1.jpg


binární
src/assets/images/Bg2.jpg


binární
src/assets/images/BgLong.jpg


binární
src/assets/images/CloseMini.png


binární
src/assets/images/DropDownArrow.png


binární
src/assets/images/Icon@2x.png


binární
src/assets/images/IconArrowRight.png


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 8
src/assets/images/IconInfo.svg


binární
src/assets/images/ImageFailed.png


binární
src/assets/images/LargeTitle1.png


binární
src/assets/images/LargeTitle2.png


binární
src/assets/images/LargeTitle3.png


binární
src/assets/images/Logo2.png


binární
src/assets/images/LogoIcon.png


binární
src/assets/images/LogoIconDark.png


binární
src/assets/images/TitleMiniHeader.png


binární
src/assets/images/about-logo.png


binární
src/assets/images/about/Banner.jpg


binární
src/assets/images/about/IconLocation.png


binární
src/assets/images/about/IconMail.png


binární
src/assets/images/about/IconMobile.png


binární
src/assets/images/about/Logo.png


binární
src/assets/images/badge.png


binární
src/assets/images/box-service.png


binární
src/assets/images/button-active.png


binární
src/assets/images/button-deactive.png


binární
src/assets/images/communicate/Banner.jpg


binární
src/assets/images/communicate/Image1.jpg


binární
src/assets/images/communicate/Image2.jpg


binární
src/assets/images/communicate/Image3.jpg


binární
src/assets/images/favicon.ico


binární
src/assets/images/favicon.png


binární
src/assets/images/footer-bg.png


binární
src/assets/images/footer/FooterPrinting.png


binární
src/assets/images/footer/GonganLogo.png


binární
src/assets/images/fusion/Banner.jpg


binární
src/assets/images/fusion/Image1.jpg


binární
src/assets/images/fusion/Image2.jpg


binární
src/assets/images/fusion/Image3.jpg


binární
src/assets/images/fusion/Image4.jpg


binární
src/assets/images/fusion/Image5.jpg


binární
src/assets/images/fusion/Image6.jpg


binární
src/assets/images/icon-contract.png


binární
src/assets/images/icon-explore.png


binární
src/assets/images/icon-join.png


binární
src/assets/images/index/Box1.png


binární
src/assets/images/index/Box2.jpg


binární
src/assets/images/index/Box3.jpg


binární
src/assets/images/index/BoxPrinting1.png


binární
src/assets/images/index/BoxPrinting2.png


binární
src/assets/images/index/BoxPrinting4.png


binární
src/assets/images/index/ButtonMore.png


binární
src/assets/images/index/Introd.jpg


binární
src/assets/images/index/IntrodLeft.png


binární
src/assets/images/index/IntrodRight.jpg


binární
src/assets/images/inheritor/Banner.jpg


binární
src/assets/images/inheritor/Image1.jpg


binární
src/assets/images/inheritor/Image10.jpg


binární
src/assets/images/inheritor/Image11.jpg


binární
src/assets/images/inheritor/Image2.jpg


binární
src/assets/images/inheritor/Image3.jpg


binární
src/assets/images/inheritor/Image4.jpg


binární
src/assets/images/inheritor/Image5.jpg


binární
src/assets/images/inheritor/Image6.jpg


binární
src/assets/images/inheritor/Image7.jpg


binární
src/assets/images/inheritor/Image8.jpg


binární
src/assets/images/inheritor/Image9.jpg


binární
src/assets/images/inheritor/LawsTest.jpg


binární
src/assets/images/inheritor/SubmitButton.png


+ 0 - 0
src/assets/images/introduction/Banner.jpg


Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů