Przeglądaj źródła

feat: 初始化Nuxt3项目,添加基本结构和多语言支持

- 新增.gitignore文件,排除不必要的文件和目录。
- 创建app.vue作为应用的主入口,包含NuxtLayout和NuxtPage。
- 添加error.vue组件,用于处理错误页面显示。
- 配置i18n支持,新增多语言配置文件i18n.config.ts,支持中文、英文和日文。
- 更新nuxt.config.ts,集成i18n模块和相关配置。
- 新增多个页面和组件,包括关于我们、联系我们、产品列表等。
- 添加全局错误处理插件,捕获未处理的错误。
- 引入Tailwind CSS进行样式设计,创建相关的CSS文件。
- 新增favicon和robots.txt文件,优化SEO设置。
- 完成基本的项目结构,准备后续功能开发。
master
lizhuang 2 miesięcy temu
commit
c4b923cd77
50 zmienionych plików z 15774 dodań i 0 usunięć
  1. 25
    0
      .gitignore
  2. 75
    0
      README.md
  3. 22
    0
      app.vue
  4. 41
    0
      assets/css/main.css
  5. 42
    0
      assets/css/styles.css
  6. 3
    0
      assets/css/tailwind.css
  7. 7
    0
      assets/icomoon/Read Me.txt
  8. 155
    0
      assets/icomoon/demo-files/demo.css
  9. 30
    0
      assets/icomoon/demo-files/demo.js
  10. 153
    0
      assets/icomoon/demo.html
  11. BIN
      assets/icomoon/fonts/icomoon.eot
  12. 18
    0
      assets/icomoon/fonts/icomoon.svg
  13. BIN
      assets/icomoon/fonts/icomoon.ttf
  14. BIN
      assets/icomoon/fonts/icomoon.woff
  15. 1
    0
      assets/icomoon/selection.json
  16. 51
    0
      assets/icomoon/style.css
  17. 0
    0
      assets/images/brand.svg
  18. BIN
      assets/images/home-a-1.webp
  19. BIN
      assets/images/home-c-1.webp
  20. BIN
      assets/images/product.png
  21. BIN
      assets/videos/video.mp4
  22. 85
    0
      components/ErrorBoundary.vue
  23. 111
    0
      components/LanguageSwitcher.vue
  24. 94
    0
      components/TheFooter.vue
  25. 198
    0
      components/TheHeader.vue
  26. 61
    0
      composables/useErrorHandler.ts
  27. 100
    0
      composables/useSearch.ts
  28. 67
    0
      error.vue
  29. 12
    0
      i18n.config.ts
  30. 49
    0
      i18n/locales/en.ts
  31. 50
    0
      i18n/locales/ja.ts
  32. 49
    0
      i18n/locales/zh.ts
  33. 13
    0
      layouts/default.vue
  34. 56
    0
      nuxt.config.ts
  35. 12621
    0
      package-lock.json
  36. 28
    0
      package.json
  37. 88
    0
      pages/about.vue
  38. 239
    0
      pages/contact.vue
  39. 210
    0
      pages/faq/[id].vue
  40. 161
    0
      pages/faq/index.vue
  41. 512
    0
      pages/index.vue
  42. 176
    0
      pages/products/[id].vue
  43. 108
    0
      pages/products/index.vue
  44. 31
    0
      plugins/error-handler.ts
  45. 6
    0
      postcss.config.js
  46. BIN
      public/favicon.ico
  47. 2
    0
      public/robots.txt
  48. 3
    0
      server/tsconfig.json
  49. 17
    0
      tailwind.config.js
  50. 4
    0
      tsconfig.json

+ 25
- 0
.gitignore Wyświetl plik

@@ -0,0 +1,25 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist

# Node dependencies
node_modules

# Logs
logs
*.log

# Misc
.DS_Store
.fleet
.idea

# Local env files
.env
.env.*
!.env.example
.cursorrules

+ 75
- 0
README.md Wyświetl plik

@@ -0,0 +1,75 @@
# Nuxt Minimal Starter

Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.

## Setup

Make sure to install dependencies:

```bash
# npm
npm install

# pnpm
pnpm install

# yarn
yarn install

# bun
bun install
```

## Development Server

Start the development server on `http://localhost:3000`:

```bash
# npm
npm run dev

# pnpm
pnpm dev

# yarn
yarn dev

# bun
bun run dev
```

## Production

Build the application for production:

```bash
# npm
npm run build

# pnpm
pnpm build

# yarn
yarn build

# bun
bun run build
```

Locally preview production build:

```bash
# npm
npm run preview

# pnpm
pnpm preview

# yarn
yarn preview

# bun
bun run preview
```

Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.

+ 22
- 0
app.vue Wyświetl plik

@@ -0,0 +1,22 @@
<template>
<div>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</div>
</template>

<script setup lang="ts">
import { useI18n } from 'vue-i18n';
const { locale } = useI18n();

// 创建一个计算属性,用于 useHead
// 它会根据当前 locale 动态生成 head 配置
const head = computed(() => ({
htmlAttrs: {
// 将 html 标签的 lang 属性设置为当前的 locale code
lang: locale.value,
},
}));
useHead(head);
</script>

+ 41
- 0
assets/css/main.css Wyświetl plik

@@ -0,0 +1,41 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
/* 颜色变量 */
--color-primary: #3b82f6;
--color-secondary: #10b981;
--color-accent: #f59e0b;
--color-danger: #ef4444;
--color-success: #10b981;
--color-warning: #f59e0b;
--color-info: #3b82f6;
--color-dark: #111827;
--color-light: #f3f4f6;
--color-bg: #ffffff;
--color-text: #111827;
--color-text-light: #6b7280;
--color-border: #e5e7eb;
}


body {
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
color: var(--color-text);
background-color: var(--color-bg);
}

@layer components {
.btn {
@apply px-4 py-2 rounded font-medium transition duration-200;
}
.btn-primary {
@apply bg-blue-500 text-white hover:bg-blue-600;
}
.container-custom {
@apply max-w-7xl mx-auto px-4 sm:px-6 lg:px-8;
}
}

+ 42
- 0
assets/css/styles.css Wyświetl plik

@@ -0,0 +1,42 @@
@import url('https://fonts.googleapis.com/css2?family=M+PLUS+1p&family=Montserrat:ital,wght@0,100..900;1,100..900&family=Noto+Sans+JP:wght@100..900&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap');

:root {
/* 颜色变量 */
--color-primary: #3b82f6;
--color-secondary: #10b981;
--color-accent: #f59e0b;
--color-danger: #ef4444;
--color-success: #10b981;
--color-warning: #f59e0b;
--color-info: #3b82f6;
--color-dark: #111827;
--color-light: #f3f4f6;
--color-bg: #05080B;
--color-text: #fff;
--color-text-light: #6b7280;
--color-border: #e5e7eb;
}
::selection{
background-color: var(--color-accent) !important;
color: var(--color-bg) !important;
}

body {
/* 设置默认字体栈 */
font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji';
color: var(--color-text);
background-color: var(--color-bg);
/* 其他基础样式 */
font-size: 16px;
line-height: 1.6;
}


/* 仅在日语环境下应用日文字体 */
html[lang="ja"] body {
font-family: 'M PLUS 1p', sans-serif !important;
}

html[lang="en"] body {
font-family: 'Montserrat', sans-serif !important;
}

+ 3
- 0
assets/css/tailwind.css Wyświetl plik

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

+ 7
- 0
assets/icomoon/Read Me.txt Wyświetl plik

@@ -0,0 +1,7 @@
Open *demo.html* to see a list of all the glyphs in your font along with their codes/ligatures.

To use the generated font in desktop programs, you can install the TTF font. In order to copy the character associated with each icon, refer to the text box at the bottom right corner of each glyph in demo.html. The character inside this text box may be invisible; but it can still be copied. See this guide for more info: https://icomoon.io/docs/#local-fonts

You won't need any of the files located under the *demo-files* directory when including the generated font in your own projects.

You can import *selection.json* back to the IcoMoon app using the *Import Icons* button (or via Main Menu → Manage Projects) to retrieve your icon selection.

+ 155
- 0
assets/icomoon/demo-files/demo.css Wyświetl plik

@@ -0,0 +1,155 @@
body {
padding: 0;
margin: 0;
font-family: sans-serif;
font-size: 1em;
line-height: 1.5;
color: #555;
background: #fff;
}
h1 {
font-size: 1.5em;
font-weight: normal;
}
small {
font-size: .66666667em;
}
a {
color: #e74c3c;
text-decoration: none;
}
a:hover, a:focus {
box-shadow: 0 1px #e74c3c;
}
.bshadow0, input {
box-shadow: inset 0 -2px #e7e7e7;
}
input:hover {
box-shadow: inset 0 -2px #ccc;
}
input, fieldset {
font-family: sans-serif;
font-size: 1em;
margin: 0;
padding: 0;
border: 0;
}
input {
color: inherit;
line-height: 1.5;
height: 1.5em;
padding: .25em 0;
}
input:focus {
outline: none;
box-shadow: inset 0 -2px #449fdb;
}
.glyph {
font-size: 16px;
width: 15em;
padding-bottom: 1em;
margin-right: 4em;
margin-bottom: 1em;
float: left;
overflow: hidden;
}
.liga {
width: 80%;
width: calc(100% - 2.5em);
}
.talign-right {
text-align: right;
}
.talign-center {
text-align: center;
}
.bgc1 {
background: #f1f1f1;
}
.fgc1 {
color: #999;
}
.fgc0 {
color: #000;
}
p {
margin-top: 1em;
margin-bottom: 1em;
}
.mvm {
margin-top: .75em;
margin-bottom: .75em;
}
.mtn {
margin-top: 0;
}
.mtl, .mal {
margin-top: 1.5em;
}
.mbl, .mal {
margin-bottom: 1.5em;
}
.mal, .mhl {
margin-left: 1.5em;
margin-right: 1.5em;
}
.mhmm {
margin-left: 1em;
margin-right: 1em;
}
.mls {
margin-left: .25em;
}
.ptl {
padding-top: 1.5em;
}
.pbs, .pvs {
padding-bottom: .25em;
}
.pvs, .pts {
padding-top: .25em;
}
.unit {
float: left;
}
.unitRight {
float: right;
}
.size1of2 {
width: 50%;
}
.size1of1 {
width: 100%;
}
.clearfix:before, .clearfix:after {
content: " ";
display: table;
}
.clearfix:after {
clear: both;
}
.hidden-true {
display: none;
}
.textbox0 {
width: 3em;
background: #f1f1f1;
padding: .25em .5em;
line-height: 1.5;
height: 1.5em;
}
#testDrive {
display: block;
padding-top: 24px;
line-height: 1.5;
}
.fs0 {
font-size: 16px;
}
.fs1 {
font-size: 32px;
}
.fs2 {
font-size: 32px;
}


+ 30
- 0
assets/icomoon/demo-files/demo.js Wyświetl plik

@@ -0,0 +1,30 @@
if (!('boxShadow' in document.body.style)) {
document.body.setAttribute('class', 'noBoxShadow');
}

document.body.addEventListener("click", function(e) {
var target = e.target;
if (target.tagName === "INPUT" &&
target.getAttribute('class').indexOf('liga') === -1) {
target.select();
}
});

(function() {
var fontSize = document.getElementById('fontSize'),
testDrive = document.getElementById('testDrive'),
testText = document.getElementById('testText');
function updateTest() {
testDrive.innerHTML = testText.value || String.fromCharCode(160);
if (window.icomoonLiga) {
window.icomoonLiga(testDrive);
}
}
function updateSize() {
testDrive.style.fontSize = fontSize.value + 'px';
}
fontSize.addEventListener('change', updateSize, false);
testText.addEventListener('input', updateTest, false);
testText.addEventListener('change', updateTest, false);
updateSize();
}());

+ 153
- 0
assets/icomoon/demo.html Wyświetl plik

@@ -0,0 +1,153 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>IcoMoon Demo</title>
<meta name="description" content="An Icon Font Generated By IcoMoon.io">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="demo-files/demo.css">
<link rel="stylesheet" href="style.css"></head>
<body>
<div class="bgc1 clearfix">
<h1 class="mhmm mvm"><span class="fgc1">Font Name:</span> icomoon <small class="fgc1">(Glyphs:&nbsp;8)</small></h1>
</div>
<div class="clearfix mhl ptl">
<h1 class="mvm mtn fgc1">Grid Size: Unknown</h1>
<div class="glyph fs1">
<div class="clearfix bshadow0 pbs">
<span class="icon-h1"></span>
<span class="mls"> icon-h1</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e904" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe904;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="clearfix bshadow0 pbs">
<span class="icon-h2"></span>
<span class="mls"> icon-h2</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e905" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe905;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="clearfix bshadow0 pbs">
<span class="icon-h3"></span>
<span class="mls"> icon-h3</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e906" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe906;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="clearfix bshadow0 pbs">
<span class="icon-arrow-left"></span>
<span class="mls"> icon-arrow-left</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e907" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe907;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="clearfix bshadow0 pbs">
<span class="icon-arrow-right"></span>
<span class="mls"> icon-arrow-right</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e900" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe900;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="clearfix bshadow0 pbs">
<span class="icon-i18n"></span>
<span class="mls"> icon-i18n</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e901" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe901;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1">
<div class="clearfix bshadow0 pbs">
<span class="icon-search"></span>
<span class="mls"> icon-search</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e902" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe902;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
</div>
<div class="clearfix mhl ptl">
<h1 class="mvm mtn fgc1">Grid Size: 12</h1>
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-brand"></span>
<span class="mls"> icon-brand</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e903" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe903;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
</div>

<!--[if gt IE 8]><!-->
<div class="mhl clearfix mbl">
<h1>Font Test Drive</h1>
<label>
Font Size: <input id="fontSize" type="number" class="textbox0 mbm"
min="8" value="48" />
px
</label>
<input id="testText" type="text" class="phl size1of1 mvl"
placeholder="Type some text to test..." value=""/>
<div id="testDrive" class="icon-" style="font-family: icomoon">&nbsp;
</div>
</div>
<!--<![endif]-->
<div class="bgc1 clearfix">
<p class="mhl">Generated by <a href="https://icomoon.io/app">IcoMoon</a></p>
</div>

<script src="demo-files/demo.js"></script>
</body>
</html>

BIN
assets/icomoon/fonts/icomoon.eot Wyświetl plik


+ 18
- 0
assets/icomoon/fonts/icomoon.svg Wyświetl plik

@@ -0,0 +1,18 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<svg xmlns="http://www.w3.org/2000/svg">
<metadata>Generated by IcoMoon</metadata>
<defs>
<font id="icomoon" horiz-adv-x="1024">
<font-face units-per-em="1024" ascent="960" descent="-64" />
<missing-glyph horiz-adv-x="1024" />
<glyph unicode="&#x20;" horiz-adv-x="512" d="" />
<glyph unicode="&#xe900;" glyph-name="arrow-right" horiz-adv-x="1182" d="M68.962 501.613h946.074l-362.27 344.949c-18.969 18.063-19.704 48.073-1.637 67.037 18.044 18.94 48.063 19.699 67.056 1.636l413.72-393.971c17.904-17.92 27.79-41.72 27.79-67.061 0-25.316-9.886-49.139-28.625-67.842l-412.909-393.167c-9.176-8.743-20.937-13.084-32.698-13.084-12.52 0-25.039 4.931-34.358 14.714-18.068 18.968-17.333 48.955 1.636 67.017l363.782 344.953h-947.563c-26.178 0-47.423 21.239-47.423 47.409s21.246 47.41 47.423 47.41z" />
<glyph unicode="&#xe901;" glyph-name="i18n" d="M512 960c282.772 0 512-229.226 512-512 0-282.772-229.228-512-512-512-282.774 0-512 229.228-512 512 0 282.774 229.226 512 512 512zM681.751 405.333h-339.5c9.429-217.793 89.813-384 169.749-384 79.912 0 160.319 166.207 169.751 384zM256.832 405.333l-169.387-0.021c15.659-157.567 117.035-289.919 256.853-349.76-50.282 85.737-82.069 210.347-87.466 349.76v0.021zM936.556 405.312h-169.387c-5.399-139.436-37.188-264.023-87.467-349.738 139.816 59.796 241.192 192.171 256.853 349.738zM344.32 840.426l-4.736-2.047c-137.408-60.757-236.672-191.915-252.117-347.712h169.387c5.398 139.435 37.184 264.043 87.467 349.76zM512 874.667l-3.2-0.085c-78.848-4.352-157.227-169.045-166.549-383.915h339.5c-9.432 217.771-89.816 384-169.751 384zM679.703 840.448l3.135-5.44c48.469-85.504 79.041-207.808 84.309-344.342h169.387c-15.639 157.589-117.038 289.941-256.853 349.781h0.023z" />
<glyph unicode="&#xe902;" glyph-name="search" d="M456.561 75.059c-254.37 0-456.561 195.953-456.561 442.472s202.191 442.469 456.561 442.469c254.37 0 456.562-195.95 456.562-442.469s-202.192-442.472-456.562-442.472zM456.561 201.479c182.624 0 326.112 139.064 326.112 316.052s-143.488 316.050-326.112 316.050c-182.625 0-326.115-139.062-326.115-316.050s143.49-316.052 326.115-316.052zM841.377 201.479l182.623-176.984-91.312-88.496-182.623 176.991 91.312 88.488z" />
<glyph unicode="&#xe903;" glyph-name="brand" horiz-adv-x="3161" d="M158.273 938.667l0.895-300.141c0.055-4.463 3.645-8.051 8.050-8.053h292.489c4.118 0 7.456 3.338 7.456 7.456v0l0.895 300.738h156.881l0.696-780.030c0-2.91-2.315-5.269-5.17-5.269h-146.144c-3.843 0-6.959 3.116-6.959 6.959v0 323.107c0 4.338-3.516 7.854-7.854 7.854h-293.48c-3.843 0-6.959-3.116-6.959-6.959v0-323.902c0-3.898-3.16-7.059-7.059-7.059v0l-152.009 0.994v774.462q1.69 8.152 12.427 9.842h145.845zM762.731 569.232c-0.028 0.005-0.060 0.011-0.093 0.017-2.264 0.44-3.751 2.588-3.321 4.798 0.011 0.055 0.022 0.11 0.035 0.164l-0.002-0.008c36.685 160.559 207.186 182.53 344.283 155.39q140.477-27.737 139.582-176.367-0.895-153.6 2.287-263.059c1.392-46.826 9.743-84.803 24.158-127.453 0.115-0.323 0.213-0.701 0.277-1.092 0.595-3.63-1.911-7.063-5.595-7.666-0.364-0.060-0.726-0.089-1.084-0.091l0.039 0.001h-129.541c-5.134 0.030-9.655 3.24-11.312 7.985l-0.022 0.068-11.831 34.597c-1.227 3.51-4.511 5.983-8.372 5.983-2.245 0-4.295-0.836-5.855-2.214l0.010 0.008c-79.534-69.493-205.297-94.546-292.586-21.971q-86.493 71.978-48.416 191.379 15.111 47.323 73.47 83.113 17.1 10.439 83.61 27.837 33.205 8.649 115.324 24.258c16.603 3.082 35.89 11.035 52.89 16.006 9.001 2.704 14.92 11.256 14.324 20.585l-0.008 0.094q-3.877 58.159-52.194 72.078-97.827 28.334-144.652-52.492c-3.716-6.326-10.487-10.506-18.234-10.506-1.179 0-2.336 0.097-3.462 0.283l0.123-0.017-113.833 18.293zM1094.189 411.556c-0.017 0.27-0.051 0.581-0.1 0.894-0.882 5.697-6.172 9.608-11.815 8.734-0.323-0.050-0.64-0.115-0.951-0.193l0.040 0.009q-56.569-13.72-125.763-30.322c-67.703-16.304-79.236-88.482-24.357-129.541 2.661-1.979 5.569-3.4 8.647-4.25l0.102-0.025q89.078-24.059 137.196 45.931c1.859 2.873 3.298 5.848 4.341 8.942l0.033 0.105q15.609 50.206 12.626 99.716zM1548.427 652.544c9.544 6.462 23.562 26.544 35.989 35.89q95.143 70.885 204.104 40.662c113.932-31.615 129.74-111.646 130.336-227.964q0.795-172.788-0.696-343.885c-0.055-2.211-1.819-3.975-3.974-3.977h-138.193c-0.075 0-0.163 0.001-0.251 0.004-4.171 0.131-7.447 3.574-7.318 7.69 0.003 0.091 0.007 0.182 0.014 0.273l-0.001-0.013q6.263 105.084-8.351 362.377-4.076 71.978-54.083 90.669c-46.925 17.497-88.581-2.784-121.985-34.398-32.311-30.521-33.703-108.464-33.504-153.202q0.696-173.583 0.298-263.257c-0.056-5.558-4.575-10.041-10.14-10.041 0 0 0 0-0.001 0h-130.336c-4.722 0-8.55 3.828-8.55 8.55v0 549.977c0 4.393 3.516 7.953 7.854 7.953v0h120.494c0.002 0 0.005 0 0.008 0 5.296 0 9.615-4.183 9.834-9.425l0.001-0.020 1.988-54.481c0.063-2.257 1.908-4.063 4.174-4.063 0.851 0 1.643 0.255 2.304 0.692l-0.015-0.010zM3161.476 462.060v-69.791l-368.242-0.895c-0.006 0-0.012 0-0.019 0-5.216 0-9.445-4.229-9.445-9.445 0-0.527 0.043-1.043 0.126-1.546l-0.007 0.055c7.953-52.89 28.433-94.944 79.435-119.102 53.188-25.153 113.137 3.579 130.137 60.545 1.869 6.252 7.569 10.731 14.314 10.731 0.843 0 1.67-0.070 2.475-0.204l-0.087 0.012 136.699-21.971c2.586-0.417 4.537-2.632 4.537-5.304 0-0.592-0.096-1.161-0.273-1.693l0.011 0.038c-30.422-90.768-104.786-151.115-200.227-164.934q-115.026-16.503-214.245 42.551c-90.868 54.183-120.892 198.736-107.769 300.539q11.831 91.365 52.791 148.729c84.405 118.108 243.374 139.98 363.271 62.335 80.628-52.294 105.979-139.781 116.517-230.649zM2808.544 491.288l178.951-0.994c0 0 0 0 0 0 8.915 0 16.148 7.199 16.205 16.1v0.005l0.099 3.977c0 0.017 0 0.037 0 0.056 0.313 59.683-45.725 108.308-102.827 108.607-0.025 0-0.050 0-0.074 0h-4.569c-57.055 0.259-103.55-47.835-103.891-107.467v-0.003l-0.099-3.977c0-0.030 0-0.064 0-0.099 0-8.95 7.255-16.205 16.205-16.205 0 0 0 0 0 0v0zM2288.59 330.729c0.092-0.239 0.243-0.476 0.449-0.68 0.742-0.734 1.906-0.759 2.601-0.057 0.212 0.214 0.356 0.472 0.433 0.747l-0.003-0.010 125.365 382.061c1.496 4.362 5.556 7.444 10.338 7.456h138.491c0.132 0.001 0.287-0.002 0.442-0.009 4.497-0.22 7.962-4.088 7.739-8.64-0.007-0.14-0.017-0.279-0.031-0.417l0.002 0.019q-1.591-15.211-7.953-32.708-106.178-289.901-220.21-599.984c-9.643-26.246-24.357-56.767-36.089-75.756-57.563-93.552-162.945-80.628-256.298-76.85-8.883 0.326-15.909 7.662-15.907 16.587v104.404c0 0.017-0.001 0.038-0.001 0.058 0 1.867 1.513 3.38 3.38 3.38 0.466 0 0.911-0.094 1.315-0.265l-0.022 0.008q9.842-4.076 43.147-6.86 84.306-6.76 112.441 74.464c8.948 25.749 10.24 38.176-0.298 65.019q-90.37 230.549-187.998 496.491-6.263 17.1-10.936 33.802c-0.134 0.447-0.211 0.96-0.211 1.491 0 2.965 2.404 5.369 5.369 5.369 0.004 0 0.009 0 0.013 0h137.99c7.574-0.045 14.243-4.806 16.671-11.83l0.031-0.1 129.74-377.19z" />
<glyph unicode="&#xe904;" glyph-name="h1" horiz-adv-x="1044" d="M713.884 365.649c8.487 0 16.626-3.371 22.626-9.372 6.002-6.002 9.374-14.141 9.374-22.628 0-61.781-33.111-106.153-78.507-133.545-44.16-26.624-100.608-37.975-154.453-37.975-8.487 0-16.626 3.373-22.628 9.374-6 6-9.372 14.141-9.372 22.626 0 8.487 3.371 16.626 9.372 22.628 6.002 6 14.141 9.372 22.628 9.372 46.080 0 90.112 9.943 121.429 28.8 30.123 18.176 47.531 43.607 47.531 78.763 0 8.487 3.371 16.626 9.372 22.628 6.002 6 14.141 9.372 22.628 9.372v-0.043zM521.929 736.004c-42.435 0-83.131-16.859-113.136-46.864-30.007-30.005-46.864-70.703-46.864-113.136v-256c0-8.487-3.371-16.626-9.372-22.628-6.002-6-14.141-9.372-22.628-9.372h-0.171c-39.841-0.018-78.255 14.832-107.729 41.641-29.472 26.809-47.882 63.649-51.628 103.316s7.442 79.301 31.376 111.153c23.932 31.854 58.888 53.63 98.028 61.074 7.101 53.884 33.546 103.349 74.411 139.183s93.361 55.59 147.712 55.59c54.351 0 106.848-19.757 147.712-55.59 40.865-35.834 67.31-85.299 74.411-139.183 39.054-7.54 73.901-29.351 97.751-61.184s34.989-71.406 31.25-111.004c-3.74-39.599-22.091-76.388-51.478-103.194-29.385-26.805-67.7-41.708-107.475-41.803h-0.171c-8.487 0-16.626 3.371-22.626 9.372-6.002 6.002-9.374 14.141-9.374 22.628v256c0 42.433-16.857 83.131-46.862 113.136-30.007 30.005-70.703 46.864-113.138 46.864zM745.929 357.465c18.718 6.624 34.922 18.889 46.383 35.102 11.459 16.215 17.613 35.582 17.613 55.438 0 19.854-6.154 39.221-17.613 55.436-11.461 16.215-27.666 28.477-46.383 35.102v-181.077zM233.929 448.004c0-41.815 26.709-77.355 64-90.539v181.077c-18.721-6.619-34.93-18.881-46.391-35.096-11.463-16.215-17.615-35.586-17.609-55.442z" />
<glyph unicode="&#xe905;" glyph-name="h2" horiz-adv-x="1044" d="M671.059 752.461c-7.629 0.006-14.948-3.017-20.352-8.405l-76.928-76.928h-215.593c-7.639 0-14.964-3.035-20.366-8.436-5.402-5.4-8.434-12.725-8.434-20.364s3.033-14.964 8.434-20.364c5.402-5.402 12.727-8.436 20.366-8.436h215.593l76.928-76.885c5.396-5.404 12.717-8.442 20.352-8.448h113.792c7.639 0 14.964 3.035 20.366 8.434 5.4 5.402 8.434 12.727 8.434 20.366 0 7.637-3.035 14.962-8.434 20.364-5.402 5.402-12.727 8.436-20.366 8.436h-101.845l-56.533 56.533 56.533 56.533h101.845c7.639 0 14.964 3.035 20.366 8.436 5.4 5.402 8.434 12.727 8.434 20.364 0 7.639-3.035 14.964-8.434 20.366-5.402 5.402-12.727 8.434-20.366 8.434h-113.792zM215.692 638.352c0.006 14.029 3.454 27.843 10.045 40.225 6.591 12.384 16.122 22.961 27.756 30.799 11.634 7.84 25.015 12.702 38.969 14.161 13.952 1.457 28.050-0.534 41.053-5.797s24.515-13.641 33.524-24.393c9.009-10.754 15.24-23.556 18.146-37.28 2.905-13.726 2.395-27.953-1.485-41.435s-11.010-25.803-20.766-35.885l98.987-210.261h308.777c31.616 0 57.259-25.6 57.259-57.259v-99.541c0-7.639-3.035-14.964-8.436-20.366-5.4-5.4-12.725-8.434-20.364-8.434h-540.459c-7.639 0-14.962 3.035-20.364 8.434-5.402 5.402-8.436 12.727-8.436 20.366v99.541c0 31.616 25.643 57.259 57.259 57.259h111.061l-86.953 184.745c-12.008-1.396-24.174-0.236-35.7 3.405-11.528 3.641-22.154 9.679-31.181 17.719-9.027 8.038-16.25 17.896-21.195 28.928-4.947 11.030-7.501 22.981-7.499 35.068zM301.367 666.47c-7.452 0-14.598-2.96-19.868-8.229-5.268-5.27-8.229-12.416-8.229-19.868s2.962-14.598 8.229-19.866c5.27-5.27 12.416-8.229 19.868-8.229s14.598 2.96 19.868 8.229c5.268 5.268 8.227 12.414 8.227 19.866s-2.96 14.598-8.227 19.868c-5.27 5.27-12.416 8.229-19.868 8.229zM443.106 310.886h-155.607v-70.4h482.859v70.4h-327.253z" />
<glyph unicode="&#xe906;" glyph-name="h3" horiz-adv-x="1044" d="M506.72 812.567c4.59 2.701 9.821 4.126 15.145 4.126 5.327 0 10.557-1.425 15.147-4.126l288-169.386c4.488-2.641 8.206-6.408 10.787-10.927 2.582-4.521 3.938-9.637 3.933-14.844v-338.816c0.006-5.205-1.351-10.323-3.933-14.842-2.582-4.521-6.3-8.288-10.787-10.929l-288-169.385c-4.59-2.702-9.821-4.126-15.147-4.126s-10.555 1.424-15.145 4.126l-288 169.385c-4.488 2.641-8.206 6.408-10.787 10.929-2.582 4.519-3.938 9.637-3.933 14.842v338.773c-0.006 5.207 1.351 10.323 3.933 14.844s6.3 8.287 10.787 10.927l288 169.43zM263.733 600.3v-304.64l258.133-151.895 258.135 151.851v304.683l-258.135 151.893-258.133-151.893zM657.347 579.499c7.92 0 15.518-3.147 21.118-8.747 5.602-5.6 8.749-13.198 8.749-21.118v-203.264c0-3.923-0.772-7.808-2.272-11.431-1.501-3.625-3.7-6.918-6.475-9.693-2.773-2.773-6.065-4.974-9.689-6.475-3.625-1.501-7.509-2.274-11.431-2.274s-7.806 0.774-11.431 2.274c-3.623 1.501-6.916 3.702-9.689 6.475-2.775 2.775-4.974 6.067-6.475 9.693-1.501 3.623-2.272 7.509-2.272 11.431v203.264c0 7.92 3.147 15.518 8.747 21.118 5.602 5.6 13.2 8.747 21.12 8.747zM521.838 511.746c7.92 0 15.518-3.147 21.118-8.749 5.6-5.6 8.747-13.198 8.747-21.118v-135.509c0-7.922-3.147-15.518-8.747-21.12-5.6-5.6-13.198-8.747-21.118-8.747-7.922 0-15.52 3.147-21.12 8.747-5.6 5.602-8.747 13.198-8.747 21.12v135.509c0 7.92 3.147 15.518 8.747 21.118 5.6 5.602 13.198 8.749 21.12 8.749zM416.195 414.123c-0.002 7.92-3.149 15.516-8.749 21.116s-13.198 8.745-21.118 8.745c-7.92 0-15.516-3.145-21.116-8.745s-8.749-13.196-8.749-21.116v-67.753c-0.002-3.923 0.77-7.808 2.271-11.431 1.501-3.625 3.7-6.918 6.475-9.693 2.773-2.773 6.065-4.974 9.691-6.475 3.623-1.501 7.507-2.274 11.429-2.274s7.808 0.774 11.431 2.274c3.623 1.501 6.916 3.702 9.691 6.475 2.773 2.775 4.972 6.067 6.473 9.693 1.501 3.623 2.272 7.509 2.272 11.431v67.753z" />
<glyph unicode="&#xe907;" glyph-name="arrow-left" horiz-adv-x="1182" d="M1113.038 501.613h-946.074l362.27 344.949c18.969 18.063 19.704 48.073 1.637 67.037-18.044 18.94-48.063 19.699-67.056 1.636l-413.72-393.971c-17.904-17.92-27.79-41.72-27.79-67.061 0-25.316 9.886-49.139 28.625-67.842l412.909-393.167c9.176-8.743 20.937-13.084 32.698-13.084 12.52 0 25.039 4.931 34.358 14.714 18.068 18.968 17.333 48.955-1.636 67.017l-363.782 344.953h947.563c26.178 0 47.423 21.239 47.423 47.409s-21.246 47.41-47.423 47.41z" />
</font></defs></svg>

BIN
assets/icomoon/fonts/icomoon.ttf Wyświetl plik


BIN
assets/icomoon/fonts/icomoon.woff Wyświetl plik


+ 1
- 0
assets/icomoon/selection.json
Plik diff jest za duży
Wyświetl plik


+ 51
- 0
assets/icomoon/style.css Wyświetl plik

@@ -0,0 +1,51 @@
@font-face {
font-family: 'icomoon';
src: url('fonts/icomoon.eot?ezztx0');
src: url('fonts/icomoon.eot?ezztx0#iefix') format('embedded-opentype'),
url('fonts/icomoon.ttf?ezztx0') format('truetype'),
url('fonts/icomoon.woff?ezztx0') format('woff'),
url('fonts/icomoon.svg?ezztx0#icomoon') format('svg');
font-weight: normal;
font-style: normal;
font-display: block;
}

[class^="icon-"], [class*=" icon-"] {
/* use !important to prevent issues with browser extensions that change fonts */
font-family: 'icomoon' !important;
speak: never;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;

/* Better Font Rendering =========== */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

.icon-h1:before {
content: "\e904";
}
.icon-h2:before {
content: "\e905";
}
.icon-h3:before {
content: "\e906";
}
.icon-arrow-left:before {
content: "\e907";
}
.icon-arrow-right:before {
content: "\e900";
}
.icon-i18n:before {
content: "\e901";
}
.icon-search:before {
content: "\e902";
}
.icon-brand:before {
content: "\e903";
}

+ 0
- 0
assets/images/brand.svg Wyświetl plik


BIN
assets/images/home-a-1.webp Wyświetl plik


BIN
assets/images/home-c-1.webp Wyświetl plik


BIN
assets/images/product.png Wyświetl plik


BIN
assets/videos/video.mp4 Wyświetl plik


+ 85
- 0
components/ErrorBoundary.vue Wyświetl plik

@@ -0,0 +1,85 @@
<template>
<div>
<div v-if="error" class="bg-red-50 border border-red-200 text-red-800 p-4 rounded-md">
<div class="flex items-start">
<div class="flex-shrink-0 mt-0.5">
<svg class="h-5 w-5 text-red-600" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium">{{ title }}</h3>
<div class="mt-1 text-sm">
<p>{{ errorMessage }}</p>
<button
v-if="retry"
@click="handleRetry"
class="mt-2 px-3 py-1 text-xs font-medium bg-red-100 text-red-800 rounded-md hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
>
{{ retryText }}
</button>
</div>
</div>
</div>
</div>
<slot v-else />
</div>
</template>

<script setup lang="ts">
/**
* 错误边界组件
* 用于捕获和显示组件内部错误
* @prop title - 错误标题
* @prop error - 错误对象
* @prop retry - 是否显示重试按钮
* @prop retryText - 重试按钮文字
* @emit retry - 点击重试按钮时触发
*/
import { computed } from 'vue';

const props = defineProps({
title: {
type: String,
default: '发生错误'
},
error: {
type: [Error, Object, null],
default: null
},
retry: {
type: Boolean,
default: false
},
retryText: {
type: String,
default: '重试'
}
});

const emit = defineEmits(['retry']);

/**
* 格式化错误信息
*/
const errorMessage = computed(() => {
if (!props.error) return '';
if (props.error instanceof Error) {
return props.error.message;
}
if (typeof props.error === 'object') {
return JSON.stringify(props.error);
}
return String(props.error);
});

/**
* 处理重试操作
*/
function handleRetry() {
emit('retry');
}
</script>

+ 111
- 0
components/LanguageSwitcher.vue Wyświetl plik

@@ -0,0 +1,111 @@
<template>
<div class="relative inline-block text-left " ref="dropdownContainerRef">
<div
@click="toggleDropdown"
class="flex justify-center items-center gap-2 text-white opacity-80 text-sm"
>
<i class="icon-i18n"></i>
{{ currentLocaleName || "Language" }}
</div>

<transition
enter-active-class="transition ease-out duration-100"
enter-from-class="transform opacity-0 scale-95"
enter-to-class="transform opacity-100 scale-100"
leave-active-class="transition ease-in duration-75"
leave-from-class="transform opacity-100 scale-100"
leave-to-class="transform opacity-0 scale-95"
>
<div
v-if="isDropdownOpen"
class="origin-top-right absolute right-0 mt-2 w-36 rounded-md shadow-lg bg-[var(--color-bg)] ring-1 ring-black ring-opacity-5 focus:outline-none z-10"
role="menu"
aria-orientation="vertical"
aria-labelledby="options-menu"
>
<div class="py-1" role="none">
<a
v-for="locale in availableLocales"
:key="locale.code"
href="#"
@click.prevent="selectLanguage(locale.code)"
:class="[
'block px-4 py-2 text-sm opacity-80',
currentLocale === locale.code
? 'text-white opacity-100 font-bold'
: 'text-white hover: opacity-100',
]"
role="menuitem"
>
{{ locale.name }}
</a>
</div>
</div>
</transition>
</div>
</template>

<script setup lang="ts">
/**
* 语言切换组件 - 下拉菜单样式
* 支持切换配置的语言
*/
import { ref, computed } from "vue";
import { useI18n } from "#imports"; // 修正 useI18n 导入
import { onClickOutside } from "@vueuse/core"; // 导入 onClickOutside

// 定义语言代码的类型,应该与 i18n 配置中的一致
type LocaleCode = "zh" | "en" | "ja"; // 你需要根据你的 i18n 配置更新这个类型

const { locale, locales, setLocale } = useI18n();
const currentLocale = computed(() => locale.value);
const isDropdownOpen = ref(false);
const dropdownContainerRef = ref(null); // 创建 ref

// 可用语言列表
const availableLocales = computed(() => {
// 确保 locales.value 是一个数组并且包含 code 和 name 属性
return (
(locales.value as Array<{ code: string; name: string }>) || []
).filter((l) => l.code && l.name);
});

// 当前选中语言的名称
const currentLocaleName = computed(() => {
const current = availableLocales.value.find(
(l) => l.code === currentLocale.value
);
return current ? current.name : "";
});

/**
* 切换下拉菜单的显示/隐藏状态
*/
function toggleDropdown() {
isDropdownOpen.value = !isDropdownOpen.value;
}

/**
* 选择语言并关闭下拉菜单
* @param {string} langCode - 选择的语言代码
*/
async function selectLanguage(langCode: string) {
if (currentLocale.value !== langCode) {
try {
// 使用类型断言,确保 langCode 是有效的 LocaleCode
await setLocale(langCode as LocaleCode);
} catch (error) {
console.error("Failed to set locale:", error);
// 这里可以添加用户反馈,例如显示一个错误提示
}
}
isDropdownOpen.value = false; // 关闭下拉菜单
}

// 点击外部关闭下拉菜单
onClickOutside(dropdownContainerRef, () => {
if (isDropdownOpen.value) {
isDropdownOpen.value = false;
}
});
</script>

+ 94
- 0
components/TheFooter.vue Wyświetl plik

@@ -0,0 +1,94 @@
<template>
<footer
class="text-white py-8 w-full relative bg-stone-950 border-t border-zinc-800 overflow-hidden"
>
<div class="max-w-screen-2xl mx-auto px-4 sm:px-6 lg:px-10">
<div
class="grid grid-cols-1 md:grid-cols-2 sm:grid-cols-1 lg:grid-cols-4 gap-8 lg:py-14"
>
<!-- Logo & 描述 -->
<div
class="flex flex-col lg:items-start :lg:justify-start sm:items-center text-center lg:text-left"
>
<h3 class="mb-4">
<i class="icon-brand text-white text-2xl"></i>
</h3>
<LanguageSwitcher class="mb-8 hidden lg:block" />
<p class="text-white opacity-60 text-xs">
&copy; {{ new Date().getFullYear() }} Hanye. All rights reserved.
</p>
</div>

<!-- 快捷链接 -->
<div class="hidden lg:block">
<h3
class="w-36 justify-start text-zinc-300 text-xl font-normal leading-snug mb-4"
>
{{ $t("common.home") }}
</h3>
<ul class="space-y-4">
<li v-for="item in menuItems" :key="item.path">
<NuxtLink
:to="item.path"
class="text-zinc-500 text-sm font-normal hover:text-white transition"
>
{{ $t(item.label) }}
</NuxtLink>
</li>
</ul>
</div>
<!-- 快捷链接 -->
<div class="hidden lg:block">
<h3
class="w-36 justify-start text-zinc-300 text-xl font-normal leading-snug mb-4"
>
{{ $t("common.home") }}
</h3>
<ul class="space-y-4">
<li v-for="item in menuItems" :key="item.path">
<NuxtLink
:to="item.path"
class="text-zinc-500 text-sm font-normal hover:text-white transition"
>
{{ $t(item.label) }}
</NuxtLink>
</li>
</ul>
</div>
<!-- 快捷链接 -->
<div class="hidden lg:block">
<h3
class="w-36 justify-start text-zinc-300 text-xl font-normal leading-snug mb-4"
>
{{ $t("common.home") }}
</h3>
<ul class="space-y-4">
<li v-for="item in menuItems" :key="item.path">
<NuxtLink
:to="item.path"
class="text-zinc-500 text-sm font-normal hover:text-white transition"
>
{{ $t(item.label) }}
</NuxtLink>
</li>
</ul>
</div>
</div>
</div>
</footer>
</template>

<script setup lang="ts">
/**
* 页脚组件
* 包含网站导航、联系信息和版权信息
*/
// 导航菜单项
const menuItems = [
{ label: "common.home", path: "/" },
{ label: "common.products", path: "/products" },
{ label: "common.faq", path: "/faq" },
{ label: "common.about", path: "/about" },
{ label: "common.contact", path: "/contact" },
];
</script>

+ 198
- 0
components/TheHeader.vue Wyświetl plik

@@ -0,0 +1,198 @@
<template>
<header class="fixed top-0 z-50 w-full bg-slate-900/70 backdrop-blur-[50px]">
<div class="max-w-screen-2xl mx-auto px-4 sm:px-6 lg:px-10">
<div class="h-[55px] flex justify-between items-center sm:h-[72px]">
<div class="flex justify-start items-center gap-12 lg:gap-24">
<nuxt-link :to="homePath" class="mt-[5px] flex-shrink-0">
<i class="icon-brand text-white text-1xl sm:text-2xl"></i>
</nuxt-link>
<!-- Desktop Menu -->
<nav class="hidden md:flex justify-start items-start gap-7 lg:gap-14">
<nuxt-link
v-for="item in menuItems"
:key="item.path"
class="justify-start text-white text-sm opacity-80 hover:opacity-100 transition-opacity"
:to="item.path"
:class="{
'text-white font-bold opacity-100': $route.path === item.path,
}"
>
{{ $t(item.label) }}
</nuxt-link>
</nav>
</div>

<div class="flex justify-start items-center gap-4 md:gap-6">
<!-- Search -->
<div
class="w-auto h-8 relative items-center opacity-40 rounded-2xl border border-color-[rgba(255,255,255,0.4)] pr-4 hover:opacity-100 transition-opacity duration-300 hidden md:flex"
>
<button
class="flex items-center justify-center w-8 h-8 opacity-80 hover:opacity-100 text-white"
>
<i class="icon-search text-sm"></i>
</button>
<span
class="hidden lg:inline-block ml-1 text-white text-sm opacity-80"
>
{{ $t("common.search") }}
</span>
<!-- Input overlay could go here if implementing search -->
</div>

<!-- Language -->
<LanguageSwitcher />

<!-- Mobile Menu Button -->
<div class="md:hidden">
<button
@click="toggleMobileMenu"
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 bg-white/10 hover:text-white hover:bg-white/20 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white"
>
<span class="sr-only">Open main menu</span>
<!-- Icon when menu is closed. -->
<svg
v-if="!mobileMenuOpen"
class="block h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
<!-- Icon when menu is open. -->
<svg
v-else
class="block h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
</div>
</div>
</div>

<!-- Mobile menu, show/hide based on menu state. -->
<transition name="slide-fade">
<div
v-if="mobileMenuOpen"
class="md:hidden bg-slate-800/90"
id="mobile-menu"
>
<div class="px-2 pt-2 pb-3 space-y-1 sm:px-3">
<nuxt-link
v-for="item in menuItems"
:key="item.path"
:to="item.path"
@click="closeMobileMenu"
class="block px-3 py-2 rounded-md text-base font-medium"
:class="[
$route.path === item.path
? 'bg-gray-900 text-white'
: 'text-gray-300 hover:bg-gray-700 hover:text-white',
]"
>{{ $t(item.label) }}</nuxt-link
>
</div>
</div>
</transition>
</header>
</template>

<script setup lang="ts">
import { ref, computed } from "vue";
import { useI18n } from "vue-i18n";
import { useRuntimeConfig } from '#app'; // 导入 useRuntimeConfig

/**
* 页面头部组件
* 包含导航菜单、语言切换和移动端响应式设计
*/
const { t, locale } = useI18n();
const config = useRuntimeConfig();
// 从运行时配置获取默认语言,如果未配置则默认为 'en'
const defaultLocale = config.public.i18n?.defaultLocale || 'en';
const mobileMenuOpen = ref(false);

// 使用 computed 来定义 homePath,根据是否为默认语言调整路径
const homePath = computed(() => {
// 如果是默认语言,路径为根路径 '/'
// 否则,路径为 '/<locale>/'
return locale.value === defaultLocale ? '/' : `/${locale.value}/`;
});


// 使用 computed 来定义 menuItems,根据是否为默认语言调整路径
const menuItems = computed(() => {
// 判断当前是否为默认语言
const isDefaultLocale = locale.value === defaultLocale;
// 如果是默认语言,路径前缀为空字符串,否则为 '/<locale>'
const prefix = isDefaultLocale ? '' : `/${locale.value}`;
return [
// 首页路径特殊处理:默认语言为 '/', 其他语言为 '/<locale>/'
{ label: "common.home", path: isDefaultLocale ? '/' : `${prefix}/` },
{ label: "common.products", path: `${prefix}/products` },
{ label: "common.faq", path: `${prefix}/faq` },
{ label: "common.about", path: `${prefix}/about` },
{ label: "common.contact", path: `${prefix}/contact` },
];
});

/**
* 切换移动端菜单显示状态
*/
function toggleMobileMenu() {
mobileMenuOpen.value = !mobileMenuOpen.value;
}

/**
* 关闭移动端菜单
*/
function closeMobileMenu() {
mobileMenuOpen.value = false;
}
</script>

<style lang="scss" scoped>
header {
user-select: none;
}

/* Transition for mobile menu */
.slide-fade-enter-active {
transition: all 0.3s ease-out;
}

.slide-fade-leave-active {
transition: all 0.3s cubic-bezier(1, 0.5, 0.8, 1);
}

.slide-fade-enter-from,
.slide-fade-leave-to {
transform: translateY(-20px);
opacity: 0;
}

/* Keep the sticky header consistent */
.sticky {
position: sticky;
}
</style>

+ 61
- 0
composables/useErrorHandler.ts Wyświetl plik

@@ -0,0 +1,61 @@
import { ref } from 'vue';

/**
* 通用错误处理钩子
* 用于处理API请求和其他异步操作中的错误
* @returns 包含错误状态和处理方法的对象
*/
export function useErrorHandler() {
const error = ref<Error | null>(null);
const isLoading = ref(false);

/**
* 重置错误状态
*/
function resetError() {
error.value = null;
}

/**
* 处理错误并更新状态
* @param err - 捕获到的错误
*/
function handleError(err: unknown) {
if (err instanceof Error) {
error.value = err;
} else if (typeof err === 'string') {
error.value = new Error(err);
} else {
error.value = new Error('发生未知错误');
}
isLoading.value = false;
console.error('错误:', error.value);
}

/**
* 包装异步函数以自动处理错误
* @param asyncFn - 需要执行的异步函数
* @returns 包装后的异步函数
*/
async function wrapAsync<T>(asyncFn: () => Promise<T>): Promise<T | null> {
try {
isLoading.value = true;
resetError();
const result = await asyncFn();
return result;
} catch (err) {
handleError(err);
return null;
} finally {
isLoading.value = false;
}
}

return {
error,
isLoading,
resetError,
handleError,
wrapAsync
};
}

+ 100
- 0
composables/useSearch.ts Wyświetl plik

@@ -0,0 +1,100 @@
import { ref, computed } from 'vue';
import { useErrorHandler } from './useErrorHandler';

/**
* 搜索结果项接口
*/
interface SearchResult {
id: number;
title: string;
description: string;
type: 'product' | 'faq' | 'page';
url: string;
}

/**
* 全局搜索功能钩子
* @returns 搜索相关状态和方法
*/
export function useSearch() {
const searchQuery = ref('');
const searchResults = ref<SearchResult[]>([]);
const { error, isLoading, wrapAsync } = useErrorHandler();
/**
* 根据查询条件过滤搜索结果
*/
const filteredResults = computed(() => {
if (!searchQuery.value.trim()) return [];
// 简单的模拟搜索实现,实际项目中应替换为真实API
return mockSearchResults.filter(item =>
item.title.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
item.description.toLowerCase().includes(searchQuery.value.toLowerCase())
);
});

/**
* 执行搜索
* @param query - 搜索关键词
*/
async function search(query: string) {
searchQuery.value = query;
// 在实际项目中,这里应该调用搜索API
await wrapAsync(async () => {
await new Promise(resolve => setTimeout(resolve, 300)); // 模拟API延迟
searchResults.value = filteredResults.value;
return searchResults.value;
});
}

/**
* 清空搜索
*/
function clearSearch() {
searchQuery.value = '';
searchResults.value = [];
}

return {
searchQuery,
searchResults: computed(() => searchResults.value),
isLoading,
error,
search,
clearSearch
};
}

// 模拟数据,实际项目中应替换为真实API数据
const mockSearchResults: SearchResult[] = [
{
id: 1,
title: '产品一',
description: '这是产品一的详细描述',
type: 'product',
url: '/products/1'
},
{
id: 2,
title: '产品二',
description: '这是产品二的详细描述',
type: 'product',
url: '/products/2'
},
{
id: 3,
title: '如何使用产品?',
description: '详细介绍产品的使用方法',
type: 'faq',
url: '/faq/1'
},
{
id: 4,
title: '关于我们',
description: '公司简介和历史',
type: 'page',
url: '/about'
}
];

+ 67
- 0
error.vue Wyświetl plik

@@ -0,0 +1,67 @@
<template>
<div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div class="max-w-md w-full text-center">
<div class="mb-6">
<h1 v-if="error.statusCode === 404" class="text-6xl font-extrabold text-blue-600">404</h1>
<h1 v-else class="text-6xl font-extrabold text-red-600">{{ error.statusCode || '错误' }}</h1>
</div>
<h2 class="text-3xl font-bold text-gray-900 mb-4">
{{ error.statusCode === 404 ? '页面未找到' : '发生错误' }}
</h2>
<p class="text-lg text-gray-600 mb-8">
{{ error.message || '很抱歉,您请求的页面不存在或发生了错误。' }}
</p>
<div class="flex justify-center space-x-4">
<button @click="handleError" class="btn btn-primary">
{{ error.statusCode === 404 ? '返回首页' : '重试' }}
</button>
<button v-if="error.statusCode !== 404" @click="goBack" class="btn bg-white text-gray-800 border border-gray-300 hover:bg-gray-50">
返回上一页
</button>
</div>
</div>
</div>
</template>

<script setup lang="ts">
/**
* 通用错误页面
* 处理各种错误状态(404、500等)
*/
interface ErrorProps {
error: {
statusCode: number;
message?: string;
};
}

const props = defineProps<ErrorProps>();

/**
* 处理错误
* 如果是404错误,返回首页;否则尝试刷新
*/
function handleError() {
if (props.error.statusCode === 404) {
navigateTo('/');
} else {
window.location.reload();
}
}

/**
* 返回上一页
*/
function goBack() {
window.history.back();
}

// 设置页面标题
useHead({
title: props.error.statusCode === 404 ? '页面未找到 - Hanye' : '发生错误 - Hanye'
});
</script>

+ 12
- 0
i18n.config.ts Wyświetl plik

@@ -0,0 +1,12 @@
export default {
seo: true,
locales: [
{ code: "zh", iso: "zh-CN", name: "简体中文", file: "zh.ts" },
{ code: "en", iso: "en-US", name: "English", file: "en.ts" },
{ code: "ja", iso: "ja-JP", name: "日本語", file: "ja.ts" },
],
defaultLocale: "zh",
lazy: true,
langDir: "./locales",
strategy: "prefix_except_default",
};

+ 49
- 0
i18n/locales/en.ts Wyświetl plik

@@ -0,0 +1,49 @@
export default {
common: {
home: 'Home',
products: 'Products',
faq: 'FAQ',
about: 'About Us',
contact: 'Contact',
search: 'Search',
language: 'Language'
},
home: {
title: 'Welcome to Hanye Website',
description: 'We provide high quality products and services',
learnMore: 'Learn More'
},
products: {
title: 'Our Products',
viewDetails: 'View Details',
consultation: 'Product consultation, quotation is welcome',
consultation_button: 'Contact Us',
usage:'Storage by Use',
usage_title:'Choose storage by use case.',
support:'Support',
support_description:'10+ years of professional tech support.',
development:'Own R&D and Manufacturing',
development_description:'We develop, manufacture, and sell memory products online.',
develop:'Innovation',
develop_description:'Developing innovative solutions continuously.',
strong_point:'Our strengths',
strong_point_title:'Our strengths/Why choose us',
},
faq: {
title: 'Frequently Asked Questions',
searchPlaceholder: 'Search questions'
},
about: {
title: 'About Us',
history: 'Company History',
team: 'Our Team',
values: 'Our Values'
},
contact: {
title: 'Contact Us',
name: 'Name',
email: 'Email',
message: 'Message',
submit: 'Submit'
}
}

+ 50
- 0
i18n/locales/ja.ts Wyświetl plik

@@ -0,0 +1,50 @@
export default {
common: {
home: 'ホーム',
products: '製品',
faq: 'よくある質問',
about: '会社概要',
contact: 'お問い合わせ',
search: '検索',
language: '言語'
},
home: {
title: 'Hanye ウェブサイトへようこそ',
description: '高品質の製品とサービスを提供しています',
learnMore: '詳細を見る'
},
products: {
title: '当社の製品',
viewDetails: '詳細を見る',
consultation: '製品に関するご相談、お見積もりはお気軽にどうぞ',
consultation_button: 'お問い合わせ',
usage:'使い方でストレージを選ぼう',
usage_title:'用途に応じてストレージ製品を選ぶ。',
support:'サポート',
support_description:'10年以上のキャリアを誇るチームが専門技術でサポート',
development:'独自に開発・製造、販売',
development_description:'メモリ及び関連製品の独自開発、製造・オンラインショップでの販売',
develop:'たゆまぬ発展へ',
develop_description:'製品の多様化を図り、新たな創意への飽くなき挑戦',
strong_point:'当社の強み',
strong_point_title:'当社の強み/選ばれる理由',
},
faq: {
title: 'よくある質問',
searchPlaceholder: '質問を検索'
},
about: {
title: '会社概要',
history: '会社の歴史',
team: 'チームメンバー',
values: '企業価値'
},
contact: {
title: 'お問い合わせ',
name: 'お名前',
email: 'メールアドレス',
message: 'メッセージ',
submit: '送信'
}
}

+ 49
- 0
i18n/locales/zh.ts Wyświetl plik

@@ -0,0 +1,49 @@
export default {
common: {
home: '首页',
products: '产品',
faq: '常见问题',
about: '关于我们',
contact: '联系我们',
search: '搜索',
language: '语言'
},
home: {
title: '欢迎来到Hanye官网',
description: '我们提供高质量的产品和服务',
learnMore: '了解更多'
},
products: {
title: '我们的产品',
viewDetails: '查看详情',
consultation: '欢迎进行产品咨询,我们将在第一时间回复您',
consultation_button: '联系我们',
usage:'使用方法选择存储',
usage_title:'根据用途选择存储产品。',
support:'支持',
support_description:'拥有10年以上经验的团队提供专业技术支持',
development:'独立开发和制造',
development_description:'独立开发和制造内存及相关产品,并在网上商店销售',
develop:'不断发展',
develop_description:'不断开发和制造产品,提供创新解决方案',
strong_point:'我们的优势',
strong_point_title:'我们的优势/选择我们的理由',
},
faq: {
title: '常见问题',
searchPlaceholder: '搜索问题'
},
about: {
title: '关于我们',
history: '公司历史',
team: '团队成员',
values: '企业价值观'
},
contact: {
title: '联系我们',
name: '姓名',
email: '邮箱',
message: '消息',
submit: '提交'
}
}

+ 13
- 0
layouts/default.vue Wyświetl plik

@@ -0,0 +1,13 @@
<template>
<div class="flex flex-col min-h-screen">
<TheHeader />
<main class="flex-grow">
<slot />
</main>
<TheFooter />
</div>
</template>

<script setup lang="ts">
// 导入组件
</script>

+ 56
- 0
nuxt.config.ts Wyświetl plik

@@ -0,0 +1,56 @@
import i18nConfig from "./i18n.config";
import type { Strategies } from "@nuxtjs/i18n";

// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: "2024-11-01",
devtools: { enabled: true },

// 添加CSS
css: [
"~/assets/css/tailwind.css",
"~/assets/css/styles.css",
"~/assets/icomoon/style.css",
],

// 模块
modules: ["@nuxtjs/i18n"],

// i18n 配置 (从外部文件加载)
i18n: {
...i18nConfig,
defaultLocale: i18nConfig.defaultLocale as 'zh' | 'en' | 'ja' | undefined,
strategy: i18nConfig.strategy as Strategies, // 显式类型转换 strategy
},

// Typescript 配置
typescript: {
strict: true,
shim: false,
},

// PostCSS配置 (统一到这里)
postcss: {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
},

// 编译配置
build: {
transpile: ["@nuxtjs/i18n"],
},

// 静态站点生成配置
nitro: {
prerender: {
crawlLinks: true,
routes: ["/"],
},
},

devServer: {
host: "0.0.0.0",
},
});

+ 12621
- 0
package-lock.json
Plik diff jest za duży
Wyświetl plik


+ 28
- 0
package.json Wyświetl plik

@@ -0,0 +1,28 @@
{
"name": "nuxt-app",
"private": true,
"type": "module",
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"dependencies": {
"@nuxtjs/i18n": "^9.5.3",
"@vueuse/core": "^13.1.0",
"nuxt": "^3.16.2",
"sass": "^1.87.0",
"swiper": "^11.2.6",
"vue": "^3.5.13",
"vue-i18n": "^11.1.3",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.10",
"autoprefixer": "^10.4.17",
"postcss": "^8.4.31",
"tailwindcss": "^3.4.0"
}
}

+ 88
- 0
pages/about.vue Wyświetl plik

@@ -0,0 +1,88 @@
<template>
<div class="py-8">
<div class="container-custom">
<h1 class="text-3xl font-bold mb-8">{{ $t('about.title') }}</h1>
<div class="bg-white border border-gray-200 rounded-lg overflow-hidden">
<div class="p-8">
<section class="mb-12">
<h2 class="text-2xl font-semibold mb-4">{{ $t('about.history') }}</h2>
<p class="text-gray-700 mb-4">
汉业科技成立于2020年,是一家专注于提供高质量产品和服务的科技公司。
从创立之初,我们就致力于通过创新技术解决客户面临的挑战,为客户创造价值。
</p>
<p class="text-gray-700">
经过多年的发展,我们已成为行业内备受认可的品牌,拥有众多忠实客户。
我们的产品覆盖了多个领域,为客户提供了全方位的解决方案。
</p>
</section>
<section class="mb-12">
<h2 class="text-2xl font-semibold mb-4">{{ $t('about.values') }}</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="bg-blue-50 p-6 rounded-lg">
<h3 class="text-xl font-medium mb-2 text-blue-700">创新精神</h3>
<p class="text-gray-700">
我们不断探索新技术、新方法,为客户提供创新的解决方案。
</p>
</div>
<div class="bg-green-50 p-6 rounded-lg">
<h3 class="text-xl font-medium mb-2 text-green-700">专业品质</h3>
<p class="text-gray-700">
我们坚持高标准,确保每一个产品都经过严格的质量控制。
</p>
</div>
<div class="bg-yellow-50 p-6 rounded-lg">
<h3 class="text-xl font-medium mb-2 text-yellow-700">客户至上</h3>
<p class="text-gray-700">
客户的满意是我们最大的追求,我们致力于超越客户期望。
</p>
</div>
</div>
</section>
<section>
<h2 class="text-2xl font-semibold mb-4">{{ $t('about.team') }}</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div class="text-center">
<div class="h-40 w-40 bg-gray-200 rounded-full mx-auto mb-4"></div>
<h3 class="text-xl font-medium">张三</h3>
<p class="text-gray-600">创始人 & CEO</p>
</div>
<div class="text-center">
<div class="h-40 w-40 bg-gray-200 rounded-full mx-auto mb-4"></div>
<h3 class="text-xl font-medium">李四</h3>
<p class="text-gray-600">技术总监</p>
</div>
<div class="text-center">
<div class="h-40 w-40 bg-gray-200 rounded-full mx-auto mb-4"></div>
<h3 class="text-xl font-medium">王五</h3>
<p class="text-gray-600">产品经理</p>
</div>
<div class="text-center">
<div class="h-40 w-40 bg-gray-200 rounded-full mx-auto mb-4"></div>
<h3 class="text-xl font-medium">赵六</h3>
<p class="text-gray-600">营销总监</p>
</div>
</div>
</section>
</div>
</div>
</div>
</div>
</template>

<script setup lang="ts">
/**
* 关于我们页面
* 展示公司历史、价值观和团队成员
*/

// SEO优化
useHead({
title: '关于我们 - Hanye',
meta: [
{ name: 'description', content: '了解我们的公司历史、企业价值观和团队成员。汉业科技致力于提供高质量的产品和服务。' }
]
});
</script>

+ 239
- 0
pages/contact.vue Wyświetl plik

@@ -0,0 +1,239 @@
<template>
<div class="py-8">
<div class="container-custom">
<h1 class="text-3xl font-bold mb-8">{{ $t('contact.title') }}</h1>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- 联系表单 -->
<div class="bg-white border border-gray-200 rounded-lg overflow-hidden">
<div class="p-8">
<h2 class="text-2xl font-semibold mb-6">给我们留言</h2>
<ErrorBoundary :error="error">
<form @submit.prevent="submitForm">
<div class="mb-4">
<label for="name" class="block text-gray-700 font-medium mb-2">{{ $t('contact.name') }}</label>
<input
type="text"
id="name"
v-model="formData.name"
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
:class="{ 'border-red-500': formErrors.name }"
required
>
<p v-if="formErrors.name" class="mt-1 text-sm text-red-600">{{ formErrors.name }}</p>
</div>
<div class="mb-4">
<label for="email" class="block text-gray-700 font-medium mb-2">{{ $t('contact.email') }}</label>
<input
type="email"
id="email"
v-model="formData.email"
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
:class="{ 'border-red-500': formErrors.email }"
required
>
<p v-if="formErrors.email" class="mt-1 text-sm text-red-600">{{ formErrors.email }}</p>
</div>
<div class="mb-6">
<label for="message" class="block text-gray-700 font-medium mb-2">{{ $t('contact.message') }}</label>
<textarea
id="message"
v-model="formData.message"
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 h-32"
:class="{ 'border-red-500': formErrors.message }"
required
></textarea>
<p v-if="formErrors.message" class="mt-1 text-sm text-red-600">{{ formErrors.message }}</p>
</div>
<div>
<button
type="submit"
class="btn btn-primary w-full"
:disabled="isLoading"
>
<span v-if="isLoading" class="flex items-center justify-center">
<span class="animate-spin h-4 w-4 border-2 border-white rounded-full border-t-transparent mr-2"></span>
提交中...
</span>
<span v-else>{{ $t('contact.submit') }}</span>
</button>
</div>
<div v-if="submitSuccess" class="mt-4 p-3 bg-green-50 text-green-800 rounded-md">
消息已成功发送,我们会尽快与您联系。
</div>
</form>
</ErrorBoundary>
</div>
</div>
<!-- 联系信息 -->
<div class="bg-white border border-gray-200 rounded-lg overflow-hidden">
<div class="p-8">
<h2 class="text-2xl font-semibold mb-6">联系方式</h2>
<div class="space-y-6">
<div class="flex items-start">
<div class="flex-shrink-0 h-10 w-10 flex items-center justify-center bg-blue-100 text-blue-600 rounded-full">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-4">
<h3 class="text-lg font-medium text-gray-900">地址</h3>
<p class="mt-1 text-gray-600">
中国上海市浦东新区张江高科技园区<br>
科技大道123号
</p>
</div>
</div>
<div class="flex items-start">
<div class="flex-shrink-0 h-10 w-10 flex items-center justify-center bg-blue-100 text-blue-600 rounded-full">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path d="M2 3a1 1 0 011-1h2.153a1 1 0 01.986.836l.74 4.435a1 1 0 01-.54 1.06l-1.548.773a11.037 11.037 0 006.105 6.105l.774-1.548a1 1 0 011.059-.54l4.435.74a1 1 0 01.836.986V17a1 1 0 01-1 1h-2C7.82 18 2 12.18 2 5V3z" />
</svg>
</div>
<div class="ml-4">
<h3 class="text-lg font-medium text-gray-900">电话</h3>
<p class="mt-1 text-gray-600">+86 123 456 7890</p>
</div>
</div>
<div class="flex items-start">
<div class="flex-shrink-0 h-10 w-10 flex items-center justify-center bg-blue-100 text-blue-600 rounded-full">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z" />
<path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z" />
</svg>
</div>
<div class="ml-4">
<h3 class="text-lg font-medium text-gray-900">邮箱</h3>
<p class="mt-1 text-gray-600">contact@example.com</p>
</div>
</div>
<div class="flex items-start">
<div class="flex-shrink-0 h-10 w-10 flex items-center justify-center bg-blue-100 text-blue-600 rounded-full">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-4">
<h3 class="text-lg font-medium text-gray-900">工作时间</h3>
<p class="mt-1 text-gray-600">
周一至周五: 9:00 - 18:00<br>
周六、周日: 休息
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>

<script setup lang="ts">
/**
* 联系我们页面
* 提供联系表单和联系信息
*/
import { ref, reactive } from 'vue';
import { useErrorHandler } from '~/composables/useErrorHandler';

const { error, isLoading, wrapAsync } = useErrorHandler();
const submitSuccess = ref(false);

// 表单数据
const formData = reactive({
name: '',
email: '',
message: ''
});

// 表单错误
const formErrors = reactive({
name: '',
email: '',
message: ''
});

/**
* 验证表单输入
* @returns 表单是否有效
*/
function validateForm(): boolean {
let isValid = true;
// 重置错误
formErrors.name = '';
formErrors.email = '';
formErrors.message = '';
// 验证姓名
if (!formData.name.trim()) {
formErrors.name = '请输入您的姓名';
isValid = false;
}
// 验证邮箱
if (!formData.email.trim()) {
formErrors.email = '请输入您的邮箱';
isValid = false;
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
formErrors.email = '请输入有效的邮箱地址';
isValid = false;
}
// 验证消息
if (!formData.message.trim()) {
formErrors.message = '请输入您的消息';
isValid = false;
}
return isValid;
}

/**
* 提交表单
*/
async function submitForm() {
// 重置成功状态
submitSuccess.value = false;
// 验证表单
if (!validateForm()) {
return;
}
// 提交表单数据
await wrapAsync(async () => {
// 模拟API请求
await new Promise(resolve => setTimeout(resolve, 1000));
// 模拟成功响应
submitSuccess.value = true;
// 清空表单
formData.name = '';
formData.email = '';
formData.message = '';
return true;
});
}

// SEO优化
useHead({
title: '联系我们 - Hanye',
meta: [
{ name: 'description', content: '联系我们获取更多信息或咨询服务。我们期待收到您的留言。' }
]
});
</script>

+ 210
- 0
pages/faq/[id].vue Wyświetl plik

@@ -0,0 +1,210 @@
<template>
<div class="py-8">
<div class="container-custom">
<div class="mb-4">
<NuxtLink to="/faq" class="text-blue-600 hover:underline flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
{{ $t('faq.title') }}
</NuxtLink>
</div>
<ErrorBoundary :error="error">
<div v-if="isLoading" class="flex justify-center py-12">
<!-- 加载中 -->
<div class="animate-spin h-8 w-8 border-4 border-blue-500 rounded-full border-t-transparent"></div>
</div>
<div v-else-if="faq" class="bg-white border border-gray-200 rounded-lg overflow-hidden">
<div class="p-8">
<div class="mb-2 flex items-center text-sm text-blue-600">
<span class="px-3 py-1 bg-blue-100 rounded-full">{{ faq.category }}</span>
</div>
<h1 class="text-3xl font-bold mb-6">{{ faq.question }}</h1>
<div class="prose max-w-none text-gray-700">
<p>{{ faq.answer }}</p>
</div>
<div class="mt-12 pt-6 border-t border-gray-200">
<h2 class="text-xl font-semibold mb-4">相关问题</h2>
<ul class="space-y-3">
<li v-for="relatedFaq in relatedFaqs" :key="relatedFaq.id">
<NuxtLink
:to="`/faq/${relatedFaq.id}`"
class="text-blue-600 hover:text-blue-800 hover:underline"
>
{{ relatedFaq.question }}
</NuxtLink>
</li>
</ul>
</div>
<div class="mt-12 pt-6 border-t border-gray-200">
<h2 class="text-xl font-semibold mb-4">没有找到您需要的答案?</h2>
<p class="text-gray-600 mb-4">
如果您有其他问题或需要更详细的信息,请随时联系我们的客服团队。
</p>
<NuxtLink to="/contact" class="btn btn-primary">
联系我们
</NuxtLink>
</div>
</div>
</div>
<div v-else class="bg-yellow-50 border border-yellow-200 text-yellow-800 p-4 rounded-md">
未找到该问题,可能已被删除或移动。
</div>
</ErrorBoundary>
</div>
</div>
</template>

<script setup lang="ts">
/**
* FAQ详情页面
* 展示单个FAQ详细内容和相关问题
*/
import { ref, computed, onMounted } from 'vue';
import { useErrorHandler } from '~/composables/useErrorHandler';

// FAQ接口定义
interface Faq {
id: number;
question: string;
answer: string;
category: string;
}

const route = useRoute();
const { error, isLoading, wrapAsync } = useErrorHandler();
const faq = ref<Faq | null>(null);
const allFaqs = ref<Faq[]>([]);

// 获取FAQ ID
const faqId = computed(() => {
const id = route.params.id;
return typeof id === 'string' ? parseInt(id, 10) : -1;
});

// 相关问题
const relatedFaqs = computed(() => {
if (!faq.value) return [];
// 查找同类别的其他问题,最多返回3个
return allFaqs.value
.filter(item => item.id !== faq.value?.id && item.category === faq.value?.category)
.slice(0, 3);
});

/**
* 加载FAQ详情数据
*/
async function loadFaqDetail() {
if (faqId.value <= 0) {
error.value = new Error('无效的FAQ ID');
return;
}
await wrapAsync(async () => {
// 模拟API请求延迟
await new Promise(resolve => setTimeout(resolve, 500));
// 模拟数据,实际项目中应从API获取
const mockFaqs = [
{
id: 1,
question: '如何使用产品?',
answer: '我们的产品设计简单直观,开箱即用。首先,打开包装并检查所有配件是否齐全。然后,按照说明书的步骤进行安装。如有任何问题,可以观看我们网站上的视频教程或联系客服。\n\n大多数产品只需简单的几个步骤即可完成安装和初始设置。我们还提供在线指南和视频教程,帮助您更好地理解产品功能和使用方法。如果您在使用过程中遇到任何困难,我们的技术支持团队随时为您提供帮助。',
category: '使用指南'
},
{
id: 2,
question: '产品有哪些保修政策?',
answer: '我们为所有产品提供1年的标准保修服务,覆盖制造缺陷和材料问题。部分高端产品可享受最长3年的延长保修服务。保修期内,我们提供免费维修或更换服务。请注意,人为损坏、不当使用或自行拆卸不在保修范围内。\n\n要享受保修服务,您需要提供有效的购买凭证和产品序列号。我们建议您在购买后立即注册产品,以便在需要时更方便地享受保修服务。',
category: '售后服务'
},
{
id: 3,
question: '如何进行退换货?',
answer: '购买后30天内,如产品未使用且包装完好,可申请无理由退换货。请保留原始包装和购买凭证,联系我们的客服团队安排退换事宜。退款将在收到退回产品并确认状态后的7个工作日内处理。\n\n对于有质量问题的产品,我们接受30天内的退换货申请。请提供产品问题的详细描述和照片证明,我们的客服团队会协助您完成退换货流程。',
category: '售后服务'
},
{
id: 4,
question: '产品支持哪些操作系统?',
answer: '我们的软件产品兼容Windows 10/11、macOS 10.15及以上版本、iOS 14及以上版本和Android 10及以上版本。硬件产品与大多数现代设备兼容,具体请查看产品详情页的技术规格部分。\n\n我们定期更新软件以确保与最新操作系统兼容。如果您使用的是较旧版本的操作系统,可能会遇到兼容性问题。我们建议您将操作系统更新到最新版本以获得最佳体验。',
category: '技术支持'
},
{
id: 5,
question: '如何联系客服?',
answer: '您可以通过多种方式联系我们的客服团队:拨打服务热线400-123-4567(工作日9:00-18:00);发送邮件至support@example.com(24小时内回复);在官网使用在线客服(工作日9:00-20:00);或填写联系表单,我们会尽快与您联系。\n\n对于紧急问题,我们建议您优先使用电话或在线客服渠道,以获得更快的响应。非工作时间提交的问题将在下一个工作日处理。',
category: '联系方式'
},
{
id: 6,
question: '是否提供国际配送服务?',
answer: '是的,我们提供国际配送服务,覆盖大部分国家和地区。国际订单的配送时间通常为7-15个工作日,具体取决于目的地和当地海关情况。国际订单可能产生额外的关税和进口费用,这些费用需由收件人承担。\n\n对于大批量的国际订单,我们提供特别的物流解决方案,请联系我们的销售团队了解详情。在特殊情况下(如假期、天气恶劣或当地政策限制),配送时间可能会延长。',
category: '配送信息'
},
{
id: 7,
question: '如何跟踪我的订单?',
answer: '下单后,您将收到一封包含订单确认和跟踪信息的电子邮件。您可以使用提供的跟踪号在我们的网站上或通过物流公司的网站查询订单状态。您也可以登录您的账户查看订单历史和当前状态。如果您有任何疑问,请联系我们的客服团队。',
category: '配送信息'
},
{
id: 8,
question: '产品可以升级吗?',
answer: '是的,我们的大部分产品支持软件升级,我们会定期发布更新以提供新功能和修复已知问题。对于硬件产品,部分型号支持组件升级,请参考产品说明书或联系技术支持了解详情。我们建议您定期检查更新,以保持产品处于最佳状态。',
category: '技术支持'
},
{
id: 9,
question: '如何获取使用指南?',
answer: '所有产品的电子版使用指南可在我们的官网下载中心获取。您也可以在产品包装内找到纸质版使用说明书。对于特定产品的详细教程和技巧,请访问我们的知识库或YouTube频道。如果您需要其他语言版本的使用指南,请联系客服。',
category: '使用指南'
}
];
// 保存所有FAQ以便于获取相关问题
allFaqs.value = mockFaqs;
const foundFaq = mockFaqs.find(item => item.id === faqId.value);
if (foundFaq) {
faq.value = foundFaq;
} else {
error.value = new Error('未找到该问题');
}
return faq.value;
});
}

// 页面加载时获取FAQ详情
onMounted(() => {
loadFaqDetail();
});

// SEO优化
useHead({
title: computed(() => faq.value ? `${faq.value.question} - FAQ - Hanye` : 'FAQ详情 - Hanye'),
meta: [
{
name: 'description',
content: computed(() => faq.value?.answer?.substring(0, 150) + '...' || '查看详细的FAQ回答和相关问题。')
}
]
});
</script>

<style>
.prose p {
margin-top: 1.25em;
margin-bottom: 1.25em;
}
</style>

+ 161
- 0
pages/faq/index.vue Wyświetl plik

@@ -0,0 +1,161 @@
<template>
<div class="py-8">
<div class="container-custom">
<h1 class="text-3xl font-bold mb-8">{{ $t('faq.title') }}</h1>
<div class="mb-8">
<div class="relative">
<input
type="text"
v-model="searchQuery"
:placeholder="$t('faq.searchPlaceholder')"
class="w-full px-4 py-3 border border-gray-300 rounded-md pl-10 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<div class="absolute left-3 top-3 text-gray-400">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
</div>
</div>
<ErrorBoundary :error="error">
<div v-if="isLoading" class="flex justify-center py-12">
<!-- 加载中 -->
<div class="animate-spin h-8 w-8 border-4 border-blue-500 rounded-full border-t-transparent"></div>
</div>
<div v-else>
<div v-if="filteredFaqs.length === 0" class="bg-yellow-50 border border-yellow-200 text-yellow-800 p-4 rounded-md">
没有找到匹配的问题,请尝试其他关键词。
</div>
<div v-else class="space-y-6">
<div v-for="faq in filteredFaqs" :key="faq.id" class="bg-white border border-gray-200 rounded-lg overflow-hidden hover:shadow-md transition-shadow">
<div class="p-6">
<NuxtLink :to="`/faq/${faq.id}`" class="block">
<h2 class="text-xl font-semibold mb-2 text-blue-700 hover:text-blue-800">{{ faq.question }}</h2>
<p class="text-gray-600 line-clamp-2">{{ faq.answer }}</p>
<div class="mt-4 flex items-center text-blue-600">
<span class="text-sm font-medium">阅读全文</span>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</div>
</NuxtLink>
</div>
</div>
</div>
</div>
</ErrorBoundary>
</div>
</div>
</template>

<script setup lang="ts">
/**
* FAQ列表页面
* 展示所有常见问题并支持搜索
*/
import { ref, computed, onMounted } from 'vue';
import { useErrorHandler } from '~/composables/useErrorHandler';

// FAQ接口定义
interface Faq {
id: number;
question: string;
answer: string;
category: string;
}

const { error, isLoading, wrapAsync } = useErrorHandler();
const faqs = ref<Faq[]>([]);
const searchQuery = ref('');

/**
* 根据搜索条件过滤FAQ
*/
const filteredFaqs = computed(() => {
if (!searchQuery.value.trim()) return faqs.value;
const query = searchQuery.value.toLowerCase();
return faqs.value.filter(faq =>
faq.question.toLowerCase().includes(query) ||
faq.answer.toLowerCase().includes(query)
);
});

/**
* 加载FAQ数据
*/
async function loadFaqs() {
await wrapAsync(async () => {
// 模拟API请求延迟
await new Promise(resolve => setTimeout(resolve, 500));
// 模拟数据,实际项目中应从API获取
faqs.value = [
{
id: 1,
question: '如何使用产品?',
answer: '我们的产品设计简单直观,开箱即用。首先,打开包装并检查所有配件是否齐全。然后,按照说明书的步骤进行安装。如有任何问题,可以观看我们网站上的视频教程或联系客服。',
category: '使用指南'
},
{
id: 2,
question: '产品有哪些保修政策?',
answer: '我们为所有产品提供1年的标准保修服务,覆盖制造缺陷和材料问题。部分高端产品可享受最长3年的延长保修服务。保修期内,我们提供免费维修或更换服务。请注意,人为损坏、不当使用或自行拆卸不在保修范围内。',
category: '售后服务'
},
{
id: 3,
question: '如何进行退换货?',
answer: '购买后30天内,如产品未使用且包装完好,可申请无理由退换货。请保留原始包装和购买凭证,联系我们的客服团队安排退换事宜。退款将在收到退回产品并确认状态后的7个工作日内处理。',
category: '售后服务'
},
{
id: 4,
question: '产品支持哪些操作系统?',
answer: '我们的软件产品兼容Windows 10/11、macOS 10.15及以上版本、iOS 14及以上版本和Android 10及以上版本。硬件产品与大多数现代设备兼容,具体请查看产品详情页的技术规格部分。',
category: '技术支持'
},
{
id: 5,
question: '如何联系客服?',
answer: '您可以通过多种方式联系我们的客服团队:拨打服务热线400-123-4567(工作日9:00-18:00);发送邮件至support@example.com(24小时内回复);在官网使用在线客服(工作日9:00-20:00);或填写联系表单,我们会尽快与您联系。',
category: '联系方式'
},
{
id: 6,
question: '是否提供国际配送服务?',
answer: '是的,我们提供国际配送服务,覆盖大部分国家和地区。国际订单的配送时间通常为7-15个工作日,具体取决于目的地和当地海关情况。国际订单可能产生额外的关税和进口费用,这些费用需由收件人承担。',
category: '配送信息'
}
];
return faqs.value;
});
}

// 页面加载时获取FAQ数据
onMounted(() => {
loadFaqs();
});

// SEO优化
useHead({
title: '常见问题 - Hanye',
meta: [
{ name: 'description', content: '浏览我们的常见问题解答,获取产品使用指南、技术支持和售后服务等相关信息。' }
]
});
</script>

<style scoped>
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>

+ 512
- 0
pages/index.vue Wyświetl plik

@@ -0,0 +1,512 @@
<template>
<div>
<!-- 轮播图 -->
<section class="max-w-full mb-12 md:mb-32">
<Swiper
:modules="[Pagination, Autoplay, EffectCreative]"
:slides-per-view="1"
:space-between="30"
:loop="true"
:pagination="{ el: '.swiper-pagination-1', clickable: true }"
:autoplay="{ delay: 5000, disableOnInteraction: false }"
effect="creative"
:creativeEffect="{
prev: {
shadow: true,
translate: ['-120%', -20, -50],
},
next: {
shadow: true,
translate: ['-20%', -20, -50],
},
}"
class="h-[320px] sm:h-[320px] md:h-[768px] lg:h-[900px] swiper-container-1"
>
<SwiperSlide>
<div
class="max-w-screen-2xl mx-auto h-full"
:style="{
backgroundImage: `url(${homeA1Webp})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
}"
></div>
</SwiperSlide>
<SwiperSlide>
<div
class="max-w-screen-2xl mx-auto h-full"
:style="{
backgroundImage: `url(${homeA1Webp})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
}"
></div>
</SwiperSlide>
<!-- Add more slides as needed -->
<div class="max-w-screen-2xl mx-auto relative">
<div
class="swiper-pagination swiper-pagination-1 text-left bottom-1 top-0"
></div>
</div>
</Swiper>
</section>

<!-- 分类产品展示 -->
<section class="max-w-full mb-12 md:mb-32">
<div class="max-w-screen-2xl mx-auto relative">
<div
class="justify-center text-cyan-400 text-base font-normal leading-tight mb-4 px-4 md:px-0"
>
{{ $t("products.usage") }}
</div>
<div
class="justify-center text-white font-normal mb-8 px-4 md:px-0 md:mb-16 text-xl sm:text-2xl md:text-4xl lg:text-6xl"
>
{{ $t("products.usage_title") }}
</div>
</div>
<div
class="max-w-screen-2xl mx-auto relative overflow-x-auto whitespace-nowrap scrollbar-hide scroll-smooth"
>
<div class="w-full mb-8 inline-flex items-center gap-4 px-4 md:px-0">
<div
class="cursor-pointer select-none px-7 py-3 rounded-full outline outline-1 outline-offset-[-1px] outline-cyan-400 inline-flex justify-center items-center gap-2.5"
>
<div
class="text-center justify-center text-cyan-400 text-sm font-normal leading-tight md:text-base"
>
外付けストレージ化
</div>
</div>

<div
class="cursor-pointer select-none px-8 py-3 rounded-full outline outline-1 outline-offset-[-1px] outline-zinc-700 inline-flex justify-center items-center gap-2.5"
>
<div
class="text-center justify-center text-zinc-300 text-sm font-normal leading-tight md:text-base"
>
外付けストレージ化
</div>
</div>

<div
class="cursor-pointer select-none px-8 py-3 rounded-full outline outline-1 outline-offset-[-1px] outline-zinc-700 inline-flex justify-center items-center gap-2.5"
>
<div
class="text-center justify-center text-zinc-300 text-sm font-normal leading-tight md:text-base"
>
外付けストレージ化
</div>
</div>
</div>
<div class="absolute right-0 top-1 z-10 lg:block hidden">
<div class="flex items-center justify-center gap-4">
<div
class="swiper-button-prev-2 bg-zinc-700 w-10 h-10 rounded-full flex items-center justify-center cursor-pointer hover:bg-zinc-600 transition-colors duration-200"
>
<i class="icon-arrow-left text-zinc-300 text-sm font-normal"></i>
</div>
<div
class="swiper-button-next-2 bg-zinc-700 w-10 h-10 rounded-full flex items-center justify-center cursor-pointer hover:bg-zinc-600 transition-colors duration-200"
>
<i class="icon-arrow-right text-zinc-300 text-sm font-normal"></i>
</div>
</div>
</div>
</div>
<div
class="w-full relative pl-[calc((100vw-1536px)/2)] 2xl:pl-[calc((100vw-1536px)/2)] lg:pl-[0] md:pl-[0] sm:pl-[0] max-w-full"
>
<div class="w-full">
<Swiper
:modules="[Navigation]"
:spaceBetween="30"
slidesPerView="auto"
:grid="{
fill: 'column',
rows: 1,
}"
:navigation="{
prevEl: '.swiper-button-prev-2',
nextEl: '.swiper-button-next-2',
}"
class="h-32 sm:h-32 md:h-64 lg:h-96 max-w-full"
>
<SwiperSlide
class="!w-32 sm:!w-32 md:!w-64 lg:!w-96 !h-32 sm:!h-32 md:!h-64 lg:!h-96"
>
<div
class="w-full h-full [background:#181818] rounded-2xl px-2 md:px-10 flex flex-col items-center justify-center"
>
<img
:src="product"
alt="h1"
class="w-full h-full object-contain"
/>
<div
class="text-center text-white text-base font-bold leading-tight mb-4 hidden md:block"
>
Hanye Q60-2TST3
</div>
<div
class="text-center justify-center text-white text-xs font-normal mb-8 leading-tight hidden md:block opacity-80"
>
2TB SSD UP TO 550MB/s
</div>
</div>
</SwiperSlide>
<SwiperSlide
class="!w-32 sm:!w-32 md:!w-64 lg:!w-96 !h-32 sm:!h-32 md:!h-64 lg:!h-96"
>
<div
class="w-full h-full [background:#181818] rounded-2xl px-2 md:px-10 flex flex-col items-center justify-center"
>
<img
:src="product"
alt="h1"
class="w-full h-full object-contain"
/>
<div
class="text-center text-white text-base font-bold leading-tight mb-4 hidden md:block"
>
Hanye Q60-2TST3
</div>
<div
class="text-center justify-center text-white text-xs font-normal mb-8 leading-tight hidden md:block opacity-80"
>
2TB SSD UP TO 550MB/s
</div>
</div>
</SwiperSlide>
<SwiperSlide
class="!w-32 sm:!w-32 md:!w-64 lg:!w-96 !h-32 sm:!h-32 md:!h-64 lg:!h-96"
>
<div
class="w-full h-full [background:#181818] rounded-2xl px-2 md:px-10 flex flex-col items-center justify-center"
>
<img
:src="product"
alt="h1"
class="w-full h-full object-contain"
/>
<div
class="text-center text-white text-base font-bold leading-tight mb-4 hidden md:block"
>
Hanye Q60-2TST3
</div>
<div
class="text-center justify-center text-white text-xs font-normal mb-8 leading-tight hidden md:block opacity-80"
>
2TB SSD UP TO 550MB/s
</div>
</div>
</SwiperSlide>
<SwiperSlide
class="!w-32 sm:!w-32 md:!w-64 lg:!w-96 !h-32 sm:!h-32 md:!h-64 lg:!h-96"
>
<div
class="w-full h-full [background:#181818] rounded-2xl px-2 md:px-10 flex flex-col items-center justify-center"
>
<img
:src="product"
alt="h1"
class="w-full h-full object-contain"
/>
<div
class="text-center text-white text-base font-bold leading-tight mb-4 hidden md:block"
>
Hanye Q60-2TST3
</div>
<div
class="text-center justify-center text-white text-xs font-normal mb-8 leading-tight hidden md:block opacity-80"
>
2TB SSD UP TO 550MB/s
</div>
</div>
</SwiperSlide>
<SwiperSlide
class="!w-32 sm:!w-32 md:!w-64 lg:!w-96 !h-32 sm:!h-32 md:!h-64 lg:!h-96"
>
<div
class="w-full h-full [background:#181818] rounded-2xl px-2 md:px-10 flex flex-col items-center justify-center"
>
<img
:src="product"
alt="h1"
class="w-full h-full object-contain"
/>
<div
class="text-center text-white text-base font-bold leading-tight mb-4 hidden md:block"
>
Hanye Q60-2TST3
</div>
<div
class="text-center justify-center text-white text-xs font-normal mb-8 leading-tight hidden md:block opacity-80"
>
2TB SSD UP TO 550MB/s
</div>
</div>
</SwiperSlide>
</Swiper>
</div>
</div>
</section>

<!-- 按用途产品展示 -->

<!-- Tag description -->
<section class="max-w-full mb-12 md:mb-32">
<div
class="max-w-screen-2xl mx-auto grid grid-cols-1 justify-items-center gap-4 sm:grid-cols-2 md:grid-cols-3"
>
<div class="inline-flex justify-start items-center gap-5 px-4">
<div
class="w-16 h-16 relative bg-gradient-to-b from-neutral-600 to-slate-400 rounded-xl border-b-[1.50px] border-neutral-700 overflow-hidden"
>
<div
class="w-12 h-12 left-[9.50px] top-[9.50px] absolute overflow-hidden flex items-center justify-center"
>
<i class="icon-h1 text-white text-5xl"></i>
</div>
</div>
<div class="inline-flex flex-col justify-center items-start flex-1">
<div
class="justify-start text-white font-medium text-1xl md:text-lg"
>
{{ $t("products.support") }}
</div>
<div
class="justify-start text-zinc-300 text-xs font-normal md:text-sm"
>
{{ $t("products.support_description") }}
</div>
</div>
</div>
<div class="inline-flex justify-start items-center gap-5 px-4">
<div
class="w-16 h-16 relative bg-gradient-to-b from-neutral-600 to-slate-400 rounded-xl border-b-[1.50px] border-neutral-700 overflow-hidden"
>
<div
class="w-12 h-12 left-[9.50px] top-[9.50px] absolute overflow-hidden flex items-center justify-center"
>
<i class="icon-h2 text-white text-5xl"></i>
</div>
</div>
<div class="inline-flex flex-col justify-center items-start flex-1">
<div
class="justify-start text-white font-medium text-1xl md:text-lg"
>
{{ $t("products.development") }}
</div>
<div
class="justify-start text-zinc-300 text-xs font-normal md:text-sm"
>
{{ $t("products.development_description") }}
</div>
</div>
</div>
<div class="inline-flex justify-start items-center gap-5 px-4">
<div
class="w-16 h-16 relative bg-gradient-to-b from-neutral-600 to-slate-400 rounded-xl border-b-[1.50px] border-neutral-700 overflow-hidden"
>
<div
class="w-12 h-12 left-[9.50px] top-[9.50px] absolute overflow-hidden flex items-center justify-center"
>
<i class="icon-h3 text-white text-5xl"></i>
</div>
</div>
<div class="inline-flex flex-col justify-center items-start flex-1">
<div
class="justify-start text-white font-medium text-1xl md:text-lg"
>
{{ $t("products.develop") }}
</div>
<div
class="justify-start text-zinc-300 text-xs font-normal md:text-sm"
>
{{ $t("products.develop_description") }}
</div>
</div>
</div>
</div>
</section>

<!-- 当社の強み -->
<section class="max-w-full mb-0 md:mb-28">
<div class="max-w-screen-2xl mx-auto relative">
<div
class="justify-center text-cyan-400 text-base font-normal leading-tight mb-4 px-4 md:px-0"
>
{{ $t("products.strong_point") }}
</div>
<div
class="justify-center text-white font-normal mb-8 px-4 md:px-0 md:mb-16 text-xl sm:text-2xl md:text-4xl lg:text-6xl"
>
{{ $t("products.strong_point_title") }}
</div>
<div
class="absolute right-0 top-1/2 -translate-y-1/2 z-10 lg:block hidden"
>
<div class="flex items-center justify-center gap-4">
<div
class="swiper-button-prev-3 bg-zinc-700 w-10 h-10 rounded-full flex items-center justify-center cursor-pointer hover:bg-zinc-600 transition-colors duration-200"
>
<i class="icon-arrow-left text-zinc-300 text-sm font-normal"></i>
</div>
<div
class="swiper-button-next-3 bg-zinc-700 w-10 h-10 rounded-full flex items-center justify-center cursor-pointer hover:bg-zinc-600 transition-colors duration-200"
>
<i class="icon-arrow-right text-zinc-300 text-sm font-normal"></i>
</div>
</div>
</div>
</div>
<div class="overflow-hidden w-full">
<div
class="w-full relative pl-[calc((100vw-1536px)/2)] 2xl:pl-[calc((100vw-1536px)/2)] lg:pl-[0] md:pl-[0] sm:pl-[0] max-w-full"
>
<Swiper
:modules="[Navigation, Pagination]"
slides-per-view="auto"
:space-between="30"
:pagination="{ el: '.swiper-pagination-3', clickable: true }"
:navigation="{
prevEl: '.swiper-button-prev-3',
nextEl: '.swiper-button-next-3',
}"
class="h-[320px] sm:h-[320px] md:h-[480px] lg:h-[720px] max-w-full"
>
<SwiperSlide class="!max-w-screen-2xl !w-full">
<div
class="w-full h-full flex items-center px-4 md:px-20"
:style="{
backgroundImage: `url(${homeC1Webp})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
}"
>
<div
class="w-[100%] md:w-[50%] bg-white/5 rounded-2xl backdrop-blur-[50px] px-8 py-12 flex flex-col gap-8 border border-white/10"
>
<div
class="opacity-90 justify-start text-white text-2xl font-normal md:text-4xl"
>
一貫体制による高品質と安定供給
</div>
<div
class="opacity-70 justify-start text-white text-lg font-normal leading-relaxed hidden sm:block"
>
「企画・開発から製造、品質管理、販売、オンラインショップ運営まで自社で完結。ISO認証取得の工場で生産された信頼性の高い製品を、安定してお届けします。」
</div>
</div>
</div>
</SwiperSlide>
</Swiper>
</div>
<div class="max-w-screen-2xl mx-auto relative">
<div class="swiper-pagination swiper-pagination-3"></div>
</div>
</div>
</section>

<!-- 产品咨询 -->
<section
class="max-w-full h-[240px] md:h-[480px] bg-black/80 hidden md:block"
>
<div class="h-full relative">
<h1
class="text-center justify-start text-white font-normal absolute top-1/3 left-4 right-4 -translate-x-0 md:left-1/2 md:-translate-x-1/2 md:right-auto text-xl sm:text-2xl md:text-3xl"
>
{{ $t("products.consultation") }}
</h1>
<div
class="w-40 h-11 bg-zinc-300/10 rounded-lg outline outline-1 flex items-center justify-center gap-2 outline-white/20 backdrop-blur-[10px] absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/5"
>
<span class="text-white text-sm font-normal">{{
$t("products.consultation_button")
}}</span>
<i class="icon-arrow-right text-white text-sm font-normal"></i>
</div>
<video
:src="videoSrc"
autoplay
muted
loop
class="w-full h-full object-cover opacity-20"
></video>
</div>
</section>
</div>
</template>

<script setup lang="ts">
import { Swiper, SwiperSlide } from "swiper/vue";
import {
Navigation,
Pagination,
Autoplay,
EffectCreative,
} from "swiper/modules";
import "swiper/css";
import "swiper/css/navigation";
import "swiper/css/pagination";

import video from "@/assets/videos/video.mp4";
import homeA1Webp from "@/assets/images/home-a-1.webp";
import homeC1Webp from "@/assets/images/home-c-1.webp";
import product from "@/assets/images/product.png";

const videoSrc = ref(video);

/**
* 网站首页
* 展示网站主要内容和精选产品
*/
// 模拟数据
const featuredProducts = [
{
id: 1,
title: "产品一",
description: "这是产品一的详细描述,介绍产品特点和用途。",
},
{
id: 2,
title: "产品二",
description: "这是产品二的详细描述,介绍产品特点和用途。",
},
{
id: 3,
title: "产品三",
description: "这是产品三的详细描述,介绍产品特点和用途。",
},
];

// SEO优化
useHead({
title: "Hanye - 首页",
meta: [
{
name: "description",
content: "基于 Nuxt3 的静态网站脚手架,支持多语言(中文、英文、日文)。",
},
],
});
</script>

<style lang="scss" scoped>
:deep(.swiper-pagination-3) {
margin: auto;
position: static;
padding: 1rem 0;
}
:deep(.swiper-pagination-bullet) {
background-color: var(--color-bg); /* Example color */
border: 2px solid var(--color-text);
}
:deep(.swiper-pagination-bullet-active) {
background-color: var(--color-text); /* Example color */
}
</style>

+ 176
- 0
pages/products/[id].vue Wyświetl plik

@@ -0,0 +1,176 @@
<template>
<div class="py-8">
<div class="container-custom">
<div class="mb-4">
<NuxtLink to="/products" class="text-blue-600 hover:underline flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
{{ $t('products.title') }}
</NuxtLink>
</div>
<ErrorBoundary :error="error">
<div v-if="isLoading" class="flex justify-center py-12">
<!-- 加载中 -->
<div class="animate-spin h-8 w-8 border-4 border-blue-500 rounded-full border-t-transparent"></div>
</div>
<div v-else-if="product" class="bg-white border border-gray-200 rounded-lg overflow-hidden">
<div class="h-64 bg-gray-100 flex items-center justify-center">
<!-- 产品图片占位符 -->
<span class="text-gray-400 text-xl">{{ product.title }}</span>
</div>
<div class="p-8">
<h1 class="text-3xl font-bold mb-4">{{ product.title }}</h1>
<p class="text-gray-600 mb-6">{{ product.description }}</p>
<div class="border-t border-gray-200 pt-6 mt-6">
<h2 class="text-xl font-semibold mb-4">产品特点</h2>
<ul class="list-disc pl-5 space-y-2">
<li>高品质材料,经久耐用</li>
<li>精心设计,使用便捷</li>
<li>多种配置可选,满足不同需求</li>
<li>售后服务完善,解决后顾之忧</li>
</ul>
</div>
<div class="border-t border-gray-200 pt-6 mt-6">
<h2 class="text-xl font-semibold mb-4">技术规格</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="flex items-center">
<span class="font-medium mr-2">尺寸:</span>
<span>200 x 300 x 100 mm</span>
</div>
<div class="flex items-center">
<span class="font-medium mr-2">重量:</span>
<span>2.5 kg</span>
</div>
<div class="flex items-center">
<span class="font-medium mr-2">材质:</span>
<span>高级金属合金</span>
</div>
<div class="flex items-center">
<span class="font-medium mr-2">颜色:</span>
<span>银色、黑色、金色</span>
</div>
</div>
</div>
<div class="mt-8">
<button class="btn btn-primary">
联系我们了解更多
</button>
</div>
</div>
</div>
<div v-else class="bg-yellow-50 border border-yellow-200 text-yellow-800 p-4 rounded-md">
产品不存在或已被删除
</div>
</ErrorBoundary>
</div>
</div>
</template>

<script setup lang="ts">
/**
* 产品详情页面
* 展示单个产品的详细信息
*/
import { ref, onMounted } from 'vue';
import { useErrorHandler } from '~/composables/useErrorHandler';

// 产品接口定义
interface Product {
id: number;
title: string;
description: string;
}

const route = useRoute();
const { error, isLoading, wrapAsync } = useErrorHandler();
const product = ref<Product | null>(null);

// 获取产品ID
const productId = computed(() => {
const id = route.params.id;
return typeof id === 'string' ? parseInt(id, 10) : -1;
});

/**
* 加载产品详情数据
*/
async function loadProduct() {
if (productId.value <= 0) {
error.value = new Error('无效的产品ID');
return;
}
await wrapAsync(async () => {
// 模拟API请求延迟
await new Promise(resolve => setTimeout(resolve, 500));
// 模拟数据,实际项目中应从API获取
const mockProducts = [
{
id: 1,
title: '产品一',
description: '这是产品一的详细描述,介绍了产品的功能、特点和适用场景。产品一是我们公司的明星产品,采用了最新技术,具有高效、稳定的特点,广泛应用于各种场景。'
},
{
id: 2,
title: '产品二',
description: '这是产品二的详细描述,介绍了产品的功能、特点和适用场景。产品二是我们的经济型产品,性价比高,适合中小型企业使用。'
},
{
id: 3,
title: '产品三',
description: '这是产品三的详细描述,介绍了产品的功能、特点和适用场景。产品三专为高端用户设计,提供了全方位的定制服务和专业支持。'
},
{
id: 4,
title: '产品四',
description: '这是产品四的详细描述,介绍产品的功能、特点和适用场景。产品四采用模块化设计,可以根据需求进行灵活配置。'
},
{
id: 5,
title: '产品五',
description: '这是产品五的详细描述,介绍产品的功能、特点和适用场景。产品五是新一代智能产品,具有自学习能力和远程控制功能。'
},
{
id: 6,
title: '产品六',
description: '这是产品六的详细描述,介绍产品的功能、特点和适用场景。产品六是我们的入门级产品,简单易用,适合初学者。'
}
];
const foundProduct = mockProducts.find(p => p.id === productId.value);
if (foundProduct) {
product.value = foundProduct;
} else {
error.value = new Error('未找到该产品');
}
return product.value;
});
}

// 页面加载时获取产品数据
onMounted(() => {
loadProduct();
});

// SEO优化
useHead({
title: computed(() => product.value ? `${product.value.title} - Hanye` : '产品详情 - Hanye'),
meta: [
{
name: 'description',
content: computed(() => product.value?.description || '查看产品详细信息、特点和技术规格。')
}
]
});
</script>

+ 108
- 0
pages/products/index.vue Wyświetl plik

@@ -0,0 +1,108 @@
<template>
<div class="py-8">
<div class="container-custom">
<h1 class="text-3xl font-bold mb-8">{{ $t('products.title') }}</h1>
<ErrorBoundary :error="error">
<div v-if="isLoading" class="flex justify-center py-12">
<!-- 加载中 -->
<div class="animate-spin h-8 w-8 border-4 border-blue-500 rounded-full border-t-transparent"></div>
</div>
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
<div v-for="product in products" :key="product.id" class="bg-white border border-gray-200 rounded-lg overflow-hidden shadow-sm hover:shadow-md transition-shadow">
<div class="h-48 bg-gray-100 flex items-center justify-center">
<!-- 产品图片占位符 -->
<span class="text-gray-400">{{ product.title }}</span>
</div>
<div class="p-6">
<h2 class="text-xl font-semibold mb-2">{{ product.title }}</h2>
<p class="text-gray-600 mb-4">{{ product.description }}</p>
<NuxtLink :to="`/products/${product.id}`" class="text-blue-600 hover:text-blue-800 font-medium">
{{ $t('products.viewDetails') }}
</NuxtLink>
</div>
</div>
</div>
</ErrorBoundary>
</div>
</div>
</template>

<script setup lang="ts">
/**
* 产品列表页面
* 展示所有产品
*/
import { ref, onMounted } from 'vue';
import { useErrorHandler } from '~/composables/useErrorHandler';

// 产品接口定义
interface Product {
id: number;
title: string;
description: string;
}

const { error, isLoading, wrapAsync } = useErrorHandler();
const products = ref<Product[]>([]);

/**
* 加载产品数据
*/
async function loadProducts() {
await wrapAsync(async () => {
// 模拟API请求延迟
await new Promise(resolve => setTimeout(resolve, 500));
// 模拟数据,实际项目中应从API获取
products.value = [
{
id: 1,
title: '产品一',
description: '这是产品一的详细描述,介绍产品特点和用途。'
},
{
id: 2,
title: '产品二',
description: '这是产品二的详细描述,介绍产品特点和用途。'
},
{
id: 3,
title: '产品三',
description: '这是产品三的详细描述,介绍产品特点和用途。'
},
{
id: 4,
title: '产品四',
description: '这是产品四的详细描述,介绍产品特点和用途。'
},
{
id: 5,
title: '产品五',
description: '这是产品五的详细描述,介绍产品特点和用途。'
},
{
id: 6,
title: '产品六',
description: '这是产品六的详细描述,介绍产品特点和用途。'
}
];
return products.value;
});
}

// 页面加载时获取产品数据
onMounted(() => {
loadProducts();
});

// SEO优化
useHead({
title: '产品列表 - Hanye',
meta: [
{ name: 'description', content: '浏览我们的产品列表,找到适合您的解决方案。' }
]
});
</script>

+ 31
- 0
plugins/error-handler.ts Wyświetl plik

@@ -0,0 +1,31 @@
/**
* 全局错误处理插件
* 用于捕获和处理全局未捕获的错误
*/
export default defineNuxtPlugin((nuxtApp) => {
// 处理Vue组件错误
nuxtApp.vueApp.config.errorHandler = (error, instance, info) => {
console.error('Vue错误:', error);
console.error('错误信息:', info);
};

// 只在客户端环境执行以下代码
if (process.client) {
// 处理未捕获的Promise错误
window.addEventListener('unhandledrejection', (event) => {
console.error('未处理的Promise错误:', event.reason);
});

// 处理全局JS错误
window.onerror = (message, source, lineno, colno, error) => {
console.error('全局JS错误:', {
message,
source,
lineno,
colno,
error
});
return true; // 防止默认处理
};
}
});

+ 6
- 0
postcss.config.js Wyświetl plik

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

BIN
public/favicon.ico Wyświetl plik


+ 2
- 0
public/robots.txt Wyświetl plik

@@ -0,0 +1,2 @@
User-Agent: *
Disallow:

+ 3
- 0
server/tsconfig.json Wyświetl plik

@@ -0,0 +1,3 @@
{
"extends": "../.nuxt/tsconfig.server.json"
}

+ 17
- 0
tailwind.config.js Wyświetl plik

@@ -0,0 +1,17 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./components/**/*.{js,vue,ts}",
"./layouts/**/*.vue",
"./pages/**/*.vue",
"./plugins/**/*.{js,ts}",
"./app.vue",
"./error.vue"
],
theme: {
extend: {},
},
plugins: [
require('@tailwindcss/typography'),
],
}

+ 4
- 0
tsconfig.json Wyświetl plik

@@ -0,0 +1,4 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json"
}

Ładowanie…
Anuluj
Zapisz