Browse Source

feat:init

TsingfunLee 7 months ago
commit
0d5e706894
43 changed files with 1314 additions and 0 deletions
  1. 29 0
      README.md
  2. 26 0
      index.html
  3. 21 0
      package.json
  4. BIN
      public/favicon.ico
  5. 9 0
      src/App.vue
  6. BIN
      src/assets/fonts/PingFang-Bold.ttf
  7. BIN
      src/assets/fonts/PingFang-Heavy.ttf
  8. BIN
      src/assets/fonts/PingFang-Medium.ttf
  9. BIN
      src/assets/fonts/Poppins-SemiBold.ttf
  10. BIN
      src/assets/fonts/SourceHanSansCN-Bold.otf
  11. BIN
      src/assets/fonts/SourceHanSansCN-ExtraLight.otf
  12. BIN
      src/assets/fonts/SourceHanSansCN-Heavy.otf
  13. BIN
      src/assets/fonts/SourceHanSansCN-Light.otf
  14. BIN
      src/assets/fonts/SourceHanSansCN-Medium.otf
  15. BIN
      src/assets/fonts/SourceHanSansCN-Normal.otf
  16. BIN
      src/assets/fonts/SourceHanSansCN-Regular.otf
  17. 18 0
      src/assets/icon/IconShirt.vue
  18. 1 0
      src/assets/icon/guider.svg
  19. 1 0
      src/assets/icon/guider2.svg
  20. BIN
      src/assets/icon/iconfont.ttf
  21. BIN
      src/assets/icon/iconfont.woff
  22. BIN
      src/assets/icon/iconfont.woff2
  23. BIN
      src/assets/img/compositioncard-bottom.png
  24. BIN
      src/assets/img/compositioncard-top.png
  25. BIN
      src/assets/img/humi2.png
  26. BIN
      src/assets/img/qrcode_example.png
  27. BIN
      src/assets/img/space-card.png
  28. 8 0
      src/assets/main.css
  29. 59 0
      src/assets/scss/fonts.scss
  30. 133 0
      src/assets/scss/iconfont.scss
  31. 9 0
      src/assets/scss/main.scss
  32. 54 0
      src/assets/scss/mixins.scss
  33. 15 0
      src/assets/scss/variable.scss
  34. 13 0
      src/main.js
  35. 24 0
      src/router/index.js
  36. 18 0
      src/util/datetime.js
  37. 81 0
      src/util/http.js
  38. 47 0
      src/util/locale.js
  39. 5 0
      src/util/material.js
  40. 266 0
      src/views/galgame/Storyboard.vue
  41. 295 0
      src/views/note/Composition.vue
  42. 157 0
      src/views/user/Space.vue
  43. 25 0
      vite.config.js

+ 29 - 0
README.md

@@ -0,0 +1,29 @@
+# screenshot
+
+This template should help get you started developing with Vue 3 in Vite.
+
+## Recommended IDE Setup
+
+[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
+
+## Customize configuration
+
+See [Vite Configuration Reference](https://vitejs.dev/config/).
+
+## Project Setup
+
+```sh
+npm install
+```
+
+### Compile and Hot-Reload for Development
+
+```sh
+npm run dev
+```
+
+### Compile and Minify for Production
+
+```sh
+npm run build
+```

+ 26 - 0
index.html

@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+  <meta charset="UTF-8">
+  <link rel="icon" href="/favicon.ico">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
+  <title>HumiHumi</title>
+</head>
+
+<script>
+  let uiWidth = 375 //标准宽度尺寸
+  function setFontSize() {
+    var fontSize = document.documentElement.clientWidth / (uiWidth / 16)
+    document.documentElement.style.fontSize = fontSize + 'px'
+  }
+  setFontSize()
+  window.addEventListener('resize', setFontSize)
+</script>
+
+<body>
+  <div id="app"></div>
+  <script type="module" src="/src/main.js"></script>
+</body>
+
+</html>

+ 21 - 0
package.json

@@ -0,0 +1,21 @@
+{
+  "name": "screenshot",
+  "version": "0.0.0",
+  "private": true,
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "qrcode": "^1.5.3",
+    "sass": "^1.69.7",
+    "vue": "^3.3.11",
+    "vue-router": "^4.2.5"
+  },
+  "devDependencies": {
+    "@vitejs/plugin-vue": "^4.5.2",
+    "vite": "^5.0.10"
+  }
+}

BIN
public/favicon.ico


+ 9 - 0
src/App.vue

@@ -0,0 +1,9 @@
+<script setup>
+import { RouterView } from 'vue-router';
+</script>
+
+<template>
+  <RouterView />
+</template>
+
+<style scoped></style>

BIN
src/assets/fonts/PingFang-Bold.ttf


BIN
src/assets/fonts/PingFang-Heavy.ttf


BIN
src/assets/fonts/PingFang-Medium.ttf


BIN
src/assets/fonts/Poppins-SemiBold.ttf


BIN
src/assets/fonts/SourceHanSansCN-Bold.otf


BIN
src/assets/fonts/SourceHanSansCN-ExtraLight.otf


BIN
src/assets/fonts/SourceHanSansCN-Heavy.otf


BIN
src/assets/fonts/SourceHanSansCN-Light.otf


BIN
src/assets/fonts/SourceHanSansCN-Medium.otf


BIN
src/assets/fonts/SourceHanSansCN-Normal.otf


BIN
src/assets/fonts/SourceHanSansCN-Regular.otf


+ 18 - 0
src/assets/icon/IconShirt.vue

@@ -0,0 +1,18 @@
+<template>
+    <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="24"
+        height="24" viewBox="0 0 24 24">
+        <defs>
+            <clipPath id="master_svg0_941_6014">
+                <rect x="0" y="0" width="24" height="24" rx="2" />
+            </clipPath>
+        </defs>
+        <g clip-path="url(#master_svg0_941_6014)">
+            <rect x="0" y="0" width="24" height="24" rx="2" fill="#242424" fill-opacity="0.10000000149011612" />
+            <g>
+                <path
+                    d="M14.0446,6.33186L16.1631,4.235805C16.4807,3.9213988,16.9959,3.9213988,17.3134,4.235805L20.762,7.647209999999999C21.0793,7.961180000000001,21.0793,8.47012,20.762,8.78408L17.6907,11.821629999999999L17.6907,19.195999999999998C17.6907,19.64,17.326700000000002,20,16.8777,20L7.1223,20C6.67332,20,6.30935,19.64,6.30935,19.195999999999998L6.30935,11.821629999999999L3.238021,8.78408C2.9206597,8.47012,2.9206597,7.961180000000001,3.238021,7.647209999999999L6.68656,4.235805C7.00411,3.9213984,7.51933,3.9213984,7.83688,4.235805L9.95543,6.33186L14.0446,6.33186ZM14.7177,7.93987L9.282309999999999,7.93987L7.26213,5.94191L4.96229,8.21565L7.93525,11.15591L7.93525,18.392L16.064799999999998,18.392L16.064799999999998,11.15591L19.0377,8.21565L16.7379,5.94191L14.7177,7.93987Z"
+                    fill="#FFFFFF" fill-opacity="1" />
+            </g>
+        </g>
+    </svg>
+</template>

+ 1 - 0
src/assets/icon/guider.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="24" height="24" viewBox="0 0 24 24"><g><g><path d="M12,21.7301C12,22.5997,13.03328,23.0549,13.67493,22.4679L19.856749999999998,16.8135C20.06386,16.624,20.181820000000002,16.3563,20.181820000000002,16.0756L20.181820000000002,2.11333C20.181820000000002,1.26918,19.20044,0.804801,18.547710000000002,1.34009L12.365887,6.40962C12.134273,6.59957,12,6.88333,12,7.18287L12,21.7301Z" fill="#DE1E64" fill-opacity="1"/><path d="M11,7.18287L11,21.7301Q11,22.3549,11.346913,22.8605Q11.668254,23.3289,12.193691,23.5603Q12.719129,23.7918,13.28158,23.7128Q13.8888,23.6276,14.34987,23.2058L20.53168,17.5513Q21.181820000000002,16.9567,21.181820000000002,16.0756L21.181820000000002,2.11333Q21.181820000000002,1.50551,20.85129,1.00785Q20.5441,0.545339,20.03726,0.305507Q19.53041,0.0656745,18.97799,0.121429Q18.383589999999998,0.181421,17.91359,0.566849L11.731774,5.63638Q11,6.23649,11,7.18287ZM19.181820000000002,16.0756L13,21.7301L13,7.18286L19.181820000000002,2.11333L19.181820000000002,16.0756Z" fill-rule="evenodd" fill="#DE1E64" fill-opacity="1"/></g><g transform="matrix(-1,0,0,1,24,0)"><path d="M11,7.18287L11,21.7301Q11,22.3549,11.346913,22.8605Q11.668254,23.3289,12.193691,23.5603Q12.719129,23.7918,13.28158,23.7128Q13.8888,23.6276,14.34987,23.2058L20.53168,17.5513Q21.181820000000002,16.9567,21.181820000000002,16.0756L21.181820000000002,2.11333Q21.181820000000002,1.50551,20.85129,1.00785Q20.5441,0.545339,20.03726,0.305507Q19.53041,0.0656745,18.97799,0.121429Q18.383589999999998,0.181421,17.91359,0.566849L11.731774,5.63638Q11,6.23649,11,7.18287ZM19.181820000000002,16.0756L13,21.7301L13,7.18286L19.181820000000002,2.11333L19.181820000000002,16.0756Z" fill-rule="evenodd" fill="#DE1E64" fill-opacity="1"/></g></g></svg>

+ 1 - 0
src/assets/icon/guider2.svg

@@ -0,0 +1 @@
+<svg id="图层_1" data-name="图层 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18.36 23.64"><defs><style>.cls-1{fill:#de1e64;fill-rule:evenodd;}</style></defs><path class="cls-1" d="M14.61,23.05a2.29,2.29,0,0,1-2.14.77c-.53,0-1.55,0-1.75,0a2,2,0,0,1-1.07-.51L3.47,17.64a2,2,0,0,1-.65-1.48V2.2A2,2,0,0,1,4,.39,1.94,1.94,0,0,1,5,.21,2,2,0,0,1,6.09.65L12,5.5l6.18-5a2,2,0,0,1,3,1.43V16.25a1.78,1.78,0,0,1-.6,1.33ZM4.82,16.16,11,21.82V7.27L4.82,2.2Z" transform="translate(-2.82 -0.18)"/></svg>

BIN
src/assets/icon/iconfont.ttf


BIN
src/assets/icon/iconfont.woff


BIN
src/assets/icon/iconfont.woff2


BIN
src/assets/img/compositioncard-bottom.png


BIN
src/assets/img/compositioncard-top.png


BIN
src/assets/img/humi2.png


BIN
src/assets/img/qrcode_example.png


BIN
src/assets/img/space-card.png


+ 8 - 0
src/assets/main.css

@@ -0,0 +1,8 @@
+html,
+body,
+#app {
+  margin: 0;
+  padding: 0;
+  width: 100vw;
+  height: 100vh;
+}

+ 59 - 0
src/assets/scss/fonts.scss

@@ -0,0 +1,59 @@
+@font-face {
+    font-family: Source Han Sans;
+    src: url('../fonts/SourceHanSansCN-Normal.otf');
+    font-weight: 350;
+    font-style: normal;
+    /* Normal */
+}
+
+@font-face {
+    font-family: Source Han Sans;
+    src: url('../fonts/SourceHanSansCN-Regular.otf');
+    font-weight: normal;
+    font-style: normal;
+    /* Regular */
+}
+
+@font-face {
+    font-family: Source Han Sans;
+    src: url('../fonts/SourceHanSansCN-Medium.otf');
+    font-weight: medium;
+    font-style: normal;
+    /* Medium */
+}
+
+@font-face {
+    font-family: Source Han Sans;
+    src: url('../fonts/SourceHanSansCN-Bold.otf');
+    font-weight: bold;
+    font-style: normal;
+    /* Bold */
+}
+
+@font-face {
+    font-family: PingFang SC-Medium;
+    src: url('../fonts/PingFang-Medium.ttf');
+    font-weight: medium;
+    font-style: normal;
+}
+
+@font-face {
+    font-family: PingFang SC-Bold;
+    src: url('../fonts/PingFang-Bold.ttf');
+    font-weight: bold;
+    font-style: normal;
+}
+
+@font-face {
+    font-family: PingFang SC-Heavy;
+    src: url('../fonts/PingFang-Heavy.ttf');
+    font-weight: bolder;
+    font-style: normal;
+}
+
+@font-face {
+    font-family: Poppins;
+    src: url('../fonts/Poppins-SemiBold.ttf');
+    font-weight: bold;
+    font-style: normal;
+}

+ 133 - 0
src/assets/scss/iconfont.scss

@@ -0,0 +1,133 @@
+//项目所用图标均上传到阿里巴巴矢量图标库,可在这里查看"https://www.iconfont.cn/manage/index?spm=a313x.home_index.i3.23.58a33a813wSbkA&manage_type=myprojects&projectId=4321507"
+
+@font-face {
+    font-family: "iconfont";
+    /* Project id 4321507 */
+    src: url('../icon/iconfont.woff2?t=1704791712935') format('woff2'),
+        url('//at.alicdn.com/t/c/font_4321507_ll3m2rscz8e.woff?t=1704791712935') format('woff'),
+        url('//at.alicdn.com/t/c/font_4321507_ll3m2rscz8e.ttf?t=1704791712935') format('truetype');
+}
+
+.iconfont {
+    font-family: "iconfont" !important;
+    font-size: 16px;
+    font-style: normal;
+    -webkit-font-smoothing: antialiased;
+    -moz-osx-font-smoothing: grayscale;
+}
+
+.icon-link-m:before {
+    content: "\e614";
+}
+
+.icon-share-circle-fill:before {
+    content: "\e615";
+}
+
+.icon-download-line:before {
+    content: "\e616";
+}
+
+.icon-share-line:before {
+    content: "\e613";
+}
+
+.icon-edit-line:before {
+    content: "\e610";
+}
+
+.icon-t-shirt-line:before {
+    content: "\e611";
+}
+
+.icon-more-2-fill:before {
+    content: "\e612";
+}
+
+.icon-check-fill:before {
+    content: "\e60f";
+}
+
+.icon-mail-line:before {
+    content: "\e60c";
+}
+
+.icon-arrow-left-s-line:before {
+    content: "\e60e";
+}
+
+.icon-arrow-left-line:before {
+    content: "\e60d";
+}
+
+.icon-loading:before {
+    content: "\e627";
+}
+
+.icon-message-2-line:before {
+    content: "\e60b";
+}
+
+.icon-file-settings-line:before {
+    content: "\e604";
+}
+
+.icon-xinshouyindao:before {
+    content: "\e605";
+}
+
+.icon-settings-line:before {
+    content: "\e606";
+}
+
+.icon-wallet-3-line:before {
+    content: "\e607";
+}
+
+.icon-shopping-bag-2-line:before {
+    content: "\e608";
+}
+
+.icon-feedback-line:before {
+    content: "\e609";
+}
+
+.icon-history-line:before {
+    content: "\e602";
+}
+
+.icon-government-line:before {
+    content: "\e603";
+}
+
+.icon-message:before {
+    content: "\e630";
+}
+
+.icon-duanxin:before {
+    content: "\e669";
+}
+
+.icon-icon_fabu:before {
+    content: "\e601";
+}
+
+.icon-aixin:before {
+    content: "\e658";
+}
+
+.icon-sousuo:before {
+    content: "\eafe";
+}
+
+.icon-touxiang:before {
+    content: "\e676";
+}
+
+.icon-jiahao:before {
+    content: "\e721";
+}
+
+.icon-plus:before {
+    content: "\e60a";
+}

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

@@ -0,0 +1,9 @@
+body{
+    font-size: 16px; //之后使用rem布局,改变htm1的font-size,需要在body重新配
+    padding: 0;
+    margin: 0;
+}
+
+::-webkit-scrollbar {
+    display: none;
+}

+ 54 - 0
src/assets/scss/mixins.scss

@@ -0,0 +1,54 @@
+@mixin flexac {
+    display: flex;
+    justify-content: space-around;
+    align-items: center;
+}
+
+@mixin flexbc {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+}
+
+@mixin flexcc {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+}
+
+@mixin flexlr {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+}
+
+@mixin flexxl {
+    display: flex;
+    justify-content: flex-start;
+    align-items: center;
+}
+
+@mixin flexee {
+    display: flex;
+    justify-content: space-evenly;
+    align-items: flex-end;
+}
+
+@mixin flexcb {
+    display: flex;
+    justify-content: center;
+    align-items: baseline;
+}
+
+@mixin flexbs {
+    display: flex;
+    justify-content: space-between;
+    align-items: flex-start;
+}
+
+@mixin flexcol {
+    display: flex;
+    justify-content: flex-start;
+    align-items: center;
+    flex-direction: column;
+}

+ 15 - 0
src/assets/scss/variable.scss

@@ -0,0 +1,15 @@
+// 定义sass参数
+
+//定义sass mixins
+@import 'mixins';
+
+//定义sass functions
+
+//定义主题色
+$red-pink: #DE1E64;
+$light-pink: #F9F3F6;
+$dark: #241917;
+$dark-grey: #3D3D3D;
+$grey: #959595;
+$light-grey: #D9D9D9;
+$white: #fff;

+ 13 - 0
src/main.js

@@ -0,0 +1,13 @@
+import './assets/scss/main.scss'
+import './assets/scss/iconfont.scss'
+import './assets/scss/fonts.scss'
+
+import { createApp } from 'vue'
+import App from './App.vue'
+import router from './router'
+
+const app = createApp(App)
+
+app.use(router)
+
+app.mount('#app')

+ 24 - 0
src/router/index.js

@@ -0,0 +1,24 @@
+import { createRouter, createWebHashHistory } from 'vue-router'
+
+const router = createRouter({
+  history: createWebHashHistory(),
+  routes: [
+    {
+      path: '/note/composition/:id/:lang',
+      name: 'note-composition',
+      component: () => import('../views/note/Composition.vue')
+    },
+    {
+      path: '/user/space/:id/:lang',
+      name: 'user-space',
+      component: () => import('../views/user/Space.vue')
+    },
+    {
+      path: '/galgame/storyboard/:id/:chapter/:storyboard/:dialogue',
+      name: 'galgame-storyboard',
+      component: () => import('../views/galgame/Storyboard.vue')
+    }
+  ]
+})
+
+export default router

+ 18 - 0
src/util/datetime.js

@@ -0,0 +1,18 @@
+const today = () => {
+    let date = new Date();
+
+    return `${date.getFullYear()}-${num2(date.getMonth() + 1)}-${num2(date.getDate())}`;
+};
+
+const minute = () => {
+    let date = new Date();
+
+    return `${date.getFullYear()}-${num2(date.getMonth() + 1)}-${num2(date.getDate())} ${num2(date.getHours())}:${num2(date.getMinutes())}`;
+};
+
+const num2 = (num) => num < 10 ? ('0' + num) : num;
+
+export {
+    today,
+    minute,
+};

+ 81 - 0
src/util/http.js

@@ -0,0 +1,81 @@
+const root = 'https://humi.lpw.plus';
+
+const service = (uri, data, success, failure) => {
+    post(uri, data, json => {
+        if (json.code === 0 && success) {
+            success(json.data);
+        } else if (failure) {
+            failure(json);
+        }
+    }, failure);
+}
+
+const post = (uri, body, success, failure) => {
+    let header = {};
+    psid(header);
+    fetch(root + uri, {
+        method: 'POST',
+        headers: header,
+        body: JSON.stringify(body)
+    }).then(response => {
+        if (uri === '/user/sign-out')
+            localStorage.removeItem('photon-session-id');
+
+        if (response.ok && success)
+            response.json().then(success);
+    }).catch(error => {
+        if (failure)
+            failure(error);
+    });
+}
+
+const upload = (name, file, success, progress) => {
+    let xhr = new XMLHttpRequest();
+    xhr.upload.addEventListener('progress', event => {
+        if (progress)
+            progress(Math.round(100 * event.loaded / event.total));
+    });
+    xhr.addEventListener('load', event => {
+        if (success)
+            success(JSON.parse(xhr.responseText));
+    });
+    xhr.open('POST', root + '/photon/ctrl-http/upload');
+    let header = {};
+    psid(header);
+    for (let key in header)
+        xhr.setRequestHeader(key, header[key]);
+    let body = new FormData();
+    body.append(name, file);
+    xhr.send(body);
+
+    return xhr;
+}
+
+const psid = (header) => {
+    localStorage.setItem('photon-session-id', '0qv1q1gsto4bq7fxtdyeyns29o9x7n8r1jmp11he17omeb6mwcdc5esn96x08mqd'); // delete-on-build
+    let psid = localStorage.getItem('photon-session-id');
+    if (!psid) {
+        psid = '';
+        while (psid.length < 64) psid += Math.random().toString(36).substring(2);
+        psid = psid.substring(0, 64);
+        localStorage.setItem('photon-session-id', psid);
+    }
+    header['photon-session-id'] = psid;
+}
+
+const url = uri => {
+    if (!uri)
+        return null;
+
+    if (uri.indexOf('://') > -1)
+        return uri;
+
+    return root + uri;
+}
+
+export {
+    post,
+    service,
+    upload,
+    url,
+}

+ 47 - 0
src/util/locale.js

@@ -0,0 +1,47 @@
+const messages = {
+    en: {
+        'note.author': 'Author',
+        'note.state': [
+            'Ongoing',
+            'Paused',
+            'Completed'
+        ],
+        'note.description': 'Work Description',
+        'note.share.title': 'Scan the code to access the work page directly.',
+        'share.share_at': 'Share',
+        'follow.fan': 'Fans',
+    },
+    jp: {
+        'note.author': '著',
+        'note.state': [
+            '連載中',
+            '一時停止',
+            '完結した'
+        ],
+        'note.description': '作品の紹介',
+        'note.share.title': 'QRコードをスキャンして作品ページへ',
+        'share.share_at': 'から共有',
+        'follow.fan': 'フォロワー',
+    },
+    zh: {
+        'note.author': '著',
+        'note.state': [
+            '连载中',
+            '暂停中',
+            '已完结'
+        ],
+        'note.description': '作品简介',
+        'note.share.title': '扫码即可直达作品页面',
+        'share.share_at': '分享于',
+        'follow.fan': '粉丝',
+    }
+};
+
+const message = (lang, key) => {
+    // console.log(`'${key}':''`);
+    return messages[lang][key] || '';
+}
+
+export {
+    message
+}

+ 5 - 0
src/util/material.js

@@ -0,0 +1,5 @@
+const material = (material) => material.uri || material.path || material;
+
+export {
+    material
+};

+ 266 - 0
src/views/galgame/Storyboard.vue

@@ -0,0 +1,266 @@
+<script setup>
+import { ref, onMounted } from 'vue';
+import { useRoute } from 'vue-router';
+import QRCode from 'qrcode';
+import { service, url } from '../../util/http';
+import { material } from '../../util/material';
+
+const route = useRoute();
+const model = ref({
+    scence: '',
+    portraitLeft: '',
+    portraitMiddle: '',
+    portraitRight: '',
+    narratage: [],
+    dialogue: {
+        avatar: '',
+        name: '',
+        role: '',
+        texts: [],
+    },
+    loads: {
+        nick: true,
+        qrcode: true,
+    },
+});
+const qrcode = ref(null);
+
+const image = (name, value) => {
+    if (!value)
+        return;
+
+    let uri = material(value);
+    if (uri) {
+        model.value[name] = uri.indexOf('/video/') === -1 ? uri : value.cover;
+        model.value.loads[name] = true;
+    }
+};
+
+const load = (name) => delete model.value.loads[name];
+
+onMounted(() => {
+    service('/note/galgame-get', { id: route.params.id, chapter: route.params.chapter }, data => {
+        let node = {};
+        for (let i = 0; i < data.length; i++) {
+            if (data[i].id === route.params.storyboard) {
+                node = data[i];
+
+                break;
+            }
+        }
+        if (!node.id) {
+            return;
+        }
+
+        image('scence', node.scence);
+        image('scence', node.cg);
+        for (let portrait of ['Left', 'Middle', 'Right']) {
+            image('portrait' + portrait, node['portrait' + portrait]);
+        }
+        if (route.params.dialogue === 'true' && node.dialogue) {
+            if (node.avatar || node.name) {
+                model.value.dialogue = {
+                    avatar: node.avatar || '',
+                    name: node.name || '',
+                    role: node.role || '',
+                    texts: node.dialogue.trim().split('\n'),
+                };
+                if (model.value.dialogue.avatar)
+                    model.value.loads['avatar'] = true;
+            } else {
+                model.value.narratage = node.dialogue.trim().split('\n');
+            }
+        }
+    });
+    service('/note/info', { id: route.params.id }, data => {
+        model.value.author = data.user.nick;
+        delete model.value.loads.nick;
+    });
+    QRCode.toCanvas(qrcode.value, 'https://humihumi.com/#/note/' + route.params.id, { width: 105, margin: 4, color: { light: '#fff', dark: '#000' } }).then(() => delete model.value.loads.qrcode);
+});
+</script>
+
+<template>
+    <div class="background"></div>
+    <div v-if="model.narratage.length > 0 || model.dialogue.texts.length > 0" class="blackbox">
+        <div class="blackbox-1"></div>
+        <div class="blackbox-2"></div>
+    </div>
+    <div v-if="model.scence" class="scence">
+        <img :src="url(model.scence)" @load="load('scence')" />
+    </div>
+    <template v-for="portrait in ['Left', 'Middle', 'Right']">
+        <div v-if="model['portrait' + portrait]" :class="'portrait portrait-' + portrait.toLocaleLowerCase()">
+            <img :src="url(model['portrait' + portrait])" @load="load('portrait' + portrait)" />
+        </div>
+    </template>
+    <div v-if="model.narratage.length > 0" class="narratage">
+        <div v-for="text in model.narratage" class="text">{{ text }}</div>
+    </div>
+    <div v-else-if="model.dialogue.texts.length > 0" class="dialogue">
+        <div v-if="model.dialogue.avatar" class="dialogue-avatar">
+            <img :src="url(model.dialogue.avatar)" @load="load('avatar')" />
+        </div>
+        <div class="dialogue-content">
+            <div class="dialogue-name-role">
+                <div v-if="model.dialogue.name" class="dialogue-name">【{{ model.dialogue.name }}】</div>
+                <div v-if="model.dialogue.role" class="dialogue-role">{{ model.dialogue.role }}</div>
+            </div>
+            <div class="dialogue-texts">
+                <div v-for="text in model.dialogue.texts" class="dialogue-text">{{ text }}</div>
+            </div>
+        </div>
+    </div>
+    <div class="info">
+        <div class="author">#{{ model.author }}</div>
+        <canvas ref="qrcode"></canvas>
+    </div>
+    <div v-if="Object.keys(model.loads).length === 0" id="done"></div>
+</template>
+
+<style scoped>
+.background,
+.scence,
+.portrait {
+    position: absolute;
+    top: 0;
+    width: 100vw;
+    height: 100vh;
+}
+
+.background {
+    background-color: #000;
+}
+
+.scence img,
+.portrait img {
+    width: 100%;
+    height: 100%;
+    object-fit: contain;
+}
+
+.portrait-left {
+    right: 25vw;
+    z-index: 2;
+}
+
+.portrait-middle {
+    left: 50vw;
+    transform: translateX(-50%);
+    z-index: 1;
+}
+
+.portrait-right {
+    left: 25vw;
+    z-index: 2;
+}
+
+.narratage,
+.dialogue {
+    position: absolute;
+    left: 3vw;
+    right: 3vw;
+    color: #fff;
+    font-family: PingFang SC-Medium;
+    text-shadow: 3px 3px 2px rgba(0, 0, 0, 0.3);
+    -webkit-text-stroke: 1.5px transparent;
+    background: #000;
+    -webkit-background-clip: text;
+    z-index: 4;
+}
+
+.narratage {
+    bottom: calc(30px + 2.25vw);
+}
+
+.narratage .text {
+    font-size: 1.75vw;
+    text-align: center;
+}
+
+.dialogue {
+    bottom: 0;
+    display: flex;
+    align-items: flex-end;
+}
+
+.dialogue-avatar img {
+    max-width: 14.375vw;
+}
+
+.dialogue-content {
+    flex-grow: 1;
+    padding-bottom: calc(30px + 2.25vw);
+}
+
+.dialogue-name-role {
+    display: flex;
+    align-items: flex-end;
+    margin-bottom: 0.5vw;
+}
+
+.dialogue-name {
+    font-size: 1.875vw;
+}
+
+.dialogue-role {
+    font-size: 1.5vw;
+}
+
+.dialogue-texts {
+    font-size: 1.75vw;
+}
+
+.info {
+    position: absolute;
+    right: 10px;
+    bottom: 10px;
+    display: flex;
+    flex-direction: column;
+    align-items: flex-end;
+    z-index: 4;
+}
+
+.info .author {
+    color: #fff;
+    font-size: 18px;
+    line-height: 26px;
+    font-weight: bold;
+    font-family: PingFang SC-Medium;
+}
+
+.info canvas {
+    display: block;
+}
+
+.blackbox {
+    position: absolute;
+    overflow: hidden;
+    width: 100vw;
+    height: auto;
+    bottom: 0;
+    z-index: 3;
+}
+
+.blackbox-1 {
+    position: relative;
+    bottom: -11.625rem;
+    width: 150%;
+    transform: translateX(-25%) translateY(45%);
+    height: 23.25rem;
+    background: rgba(0, 0, 0, 0.55);
+    filter: blur(40px);
+    z-index: 2;
+}
+
+.blackbox-2 {
+    position: relative;
+    bottom: 0rem;
+    width: 150%;
+    transform: translateX(-25%) translateY(60%);
+    height: 2.125rem;
+    background: rgba(0, 0, 0, 0.55);
+    filter: blur(20px);
+    z-index: 2;
+}
+</style>

+ 295 - 0
src/views/note/Composition.vue

@@ -0,0 +1,295 @@
+<script setup>
+import { ref, onMounted } from 'vue';
+import { useRoute } from 'vue-router';
+import QRCode from 'qrcode';
+import { message } from '../../util/locale';
+import { service, url } from '../../util/http';
+import { minute } from '../../util/datetime';
+
+const route = useRoute();
+const model = ref({
+    note: {
+        tag: {},
+        user: {},
+    },
+    cover: false,
+    qrcode: false,
+});
+const qrcode = ref(null);
+
+onMounted(() => {
+    service('/note/info', { id: route.params.id }, data => {
+        if (!data.cover)
+            data.cover = '/image/no-cover-' + route.params.lang + '.png';
+        let tag = [];
+        for (let t of (data.tag || '').split(','))
+            if (t)
+                tag.push(t);
+        data.tag = {
+            main: tag.length > 0 ? tag[0] : '',
+        };
+        if (tag.length > 0)
+            tag.splice(0, 1);
+        data.tag.minor = tag;
+        model.value.note = data;
+    });
+    QRCode.toCanvas(qrcode.value, 'https://humihumi.com/#/note/' + route.params.id, { width: 105, margin: 0 }).then(() => model.value.qrcode = true);
+});
+</script>
+
+<template>
+    <div class="background">
+        <div></div>
+        <div class="top"><img src="../../assets/img/compositioncard-top.png"></div>
+        <div class="note-container">
+            <img :src="url(model.note.cover)" class="note-bg-img" />
+            <div class="note-info">
+                <div class="cover">
+                    <img :src="url(model.note.cover)" @load="model.cover = true" />
+                </div>
+                <div class="info-container">
+                    <div class="subject">{{ model.note.subject }}</div>
+                    <div class="author-box">
+                        <div class="author" v-if="route.params.lang === 'zh' || route.params.lang === 'jp'">{{
+                            model.note.user.nick }} {{ message(route.params.lang, 'note.author') }}</div>
+                        <div class="author" v-else-if="route.params.lang === 'en'">{{ message(route.params.lang,
+                            'note.author') }} {{ model.note.user.nick }}</div>
+                    </div>
+                    <div class="publish">{{ message(route.params.lang, 'note.state')[Math.max(0, model.note.state - 1)] }}
+                    </div>
+                    <div v-if="model.note.tag" class="tag">
+                        <span v-if="model.note.tag.main" class="main-tag">{{ model.note.tag.main }}</span>
+                        <span v-for="tag in model.note.tag.minor" class="minor-tag">{{ tag }}</span>
+                    </div>
+                </div>
+            </div>
+            <div class="description">
+                <div class="title">{{ message(route.params.lang, 'note.description') }}</div>
+                <div class="content">
+                    <div v-for="line in (model.note.description || '').split('\n')">{{ line }}</div>
+                </div>
+            </div>
+        </div>
+        <div class="divider"></div>
+        <div class="share-content">
+            <div class="text">
+                <span class="share-title">{{ message(route.params.lang, 'note.share.title') }}</span>
+                <span class="share-time">{{ message(route.params.lang, 'share.share_at') }} {{ minute() }}</span>
+            </div>
+            <div class="qrcode">
+                <canvas ref="qrcode"></canvas>
+            </div>
+        </div>
+        <div class="divider"></div>
+        <div class="bottom"><img src="../../assets/img/compositioncard-bottom.png"></div>
+    </div>
+    <div v-if="model.cover && model.qrcode" id="done"></div>
+</template>
+
+<style lang="scss" scoped>
+.background {
+    width: 100vw;
+    height: auto;
+    position: relative;
+    background: linear-gradient(180deg, #241917 0%, #DE1E64 71%);
+    @include flexcc;
+    flex-direction: column;
+    row-gap: .625rem;
+
+    .top {
+        @include flexcc;
+        max-width: 90vw;
+        padding: .625rem 0;
+
+        img {
+            width: 100%;
+            height: 100%;
+        }
+    }
+
+    .note-container {
+        position: relative;
+        font-family: Source Han Sans;
+        width: 90vw;
+        height: auto;
+        background: rgba(249, 243, 246, 0.9);
+        display: flex;
+        flex-direction: column;
+        font-size: .9rem;
+        z-index: 0;
+
+        .note-bg-img {
+            position: absolute;
+            z-index: -1;
+            opacity: .3;
+            width: 100%;
+            height: 100%;
+            object-fit: cover;
+        }
+
+        .note-info {
+            display: grid;
+            grid-template-columns: 5fr 7fr;
+            justify-content: flex-start;
+            color: #241917;
+
+            .cover {
+                padding: 1rem 0 1rem 1rem;
+                max-width: 30vw;
+                overflow: hidden;
+
+                img {
+                    width: 100%;
+                    height: 100%;
+                    object-fit: cover;
+                    border-radius: .35rem;
+                }
+            }
+
+            .info-container {
+                padding: 1.5rem 1rem 1.5rem 0;
+                display: flex;
+                flex-direction: column;
+                row-gap: 5px;
+
+                .subject {
+                    font-size: 1.6rem;
+                }
+
+                .author-box {
+                    display: flex;
+                    column-gap: 10px;
+                    align-items: center;
+                    padding-right: .625rem;
+                }
+
+                .author {
+                    color: #3D3D3D;
+                }
+
+                .follow {
+                    border: 1px solid #3D3D3D;
+                    padding: 2px 8px;
+                }
+
+                .publish {
+                    color: #959595;
+                }
+
+                .tag {
+                    display: flex;
+                    flex-wrap: wrap;
+                    column-gap: .4rem;
+                    row-gap: .4rem;
+
+                    span {
+                        display: inline-box;
+                        line-height: 24px;
+                        padding: .06rem .6rem;
+                        font-size: .8rem;
+                        font-weight: lighter;
+                        text-align: center;
+                        border-radius: 1rem;
+                        color: #fff;
+
+                    }
+
+                    .main-tag {
+                        background-color: #de1e64;
+                        border: 1px solid #de1e64;
+                        box-shadow: 0px 0px 5px #de1e64;
+                    }
+
+                    .minor-tag {
+                        background-color: #d7b1c5;
+                        border: 1px solid #d7b1c5;
+                        box-shadow: 0px 0px 5px #d7b1c5;
+                    }
+
+                    .other-tag {
+                        background-color: #39C5BB;
+                        border: 1px solid #39C5BB;
+                        box-shadow: 0px 0px 5px #39C5BB;
+                    }
+                }
+            }
+        }
+
+        .description {
+            color: #241917;
+            background-color: #fff;
+            flex-grow: 1;
+            padding: 2rem;
+            border-radius: .75rem .75rem 0 0;
+
+            .title {
+                font-weight: bold;
+                font-size: 1.1rem;
+
+            }
+
+            .content {
+                font-family: PingFang SC-Medium;
+                padding-top: 1.625rem;
+                font-weight: medium;
+                line-height: 1.5;
+            }
+        }
+    }
+
+    .divider {
+        width: 90vw;
+        height: 3px;
+        background-color: #fff
+    }
+
+    .share-content {
+        @include flexbc;
+        width: 90vw;
+        background-color: #fff;
+
+        .text {
+            @include flexbs;
+            flex-direction: column;
+            padding-left: 20px;
+            height: 100px;
+
+            .share-title {
+                font-family: PingFang SC-Bold;
+                font-size: 1rem;
+                color: #212121;
+                font-weight: bold;
+            }
+
+            .share-time {
+                font-family: PingFang SC-Medium;
+                padding-right: 5px;
+                color: #606060;
+                font-size: .7rem;
+                white-space: nowrap;
+                text-align: end;
+            }
+        }
+
+        .qrcode {
+            max-width: 105px;
+            max-height: 105px;
+            padding: 20px;
+
+            canvas {
+                width: 100%;
+                height: 100%;
+            }
+        }
+    }
+
+    .bottom {
+        @include flexcc;
+
+        img {
+            width: 100%;
+            height: 100%;
+        }
+    }
+}
+</style>

+ 157 - 0
src/views/user/Space.vue

@@ -0,0 +1,157 @@
+<script setup>
+import { ref, onMounted } from 'vue';
+import { useRoute } from 'vue-router';
+import QRCode from 'qrcode';
+import { message } from '../../util/locale';
+import { service, url } from '../../util/http';
+import { minute } from '../../util/datetime';
+
+const route = useRoute();
+const model = ref({
+    user: {},
+    fans: {
+        user: 0,
+        influencer: 0,
+    },
+    fan: false,
+    qrcode: false,
+});
+const qrcode = ref(null);
+
+onMounted(() => {
+    service('/user/info/user', { user: route.params.id }, data => model.value.user = data);
+    service('/fan/count', { user: route.params.id }, data => {
+        model.value.fans = data;
+        model.value.fan = true;
+    });
+    QRCode.toCanvas(qrcode.value, 'https://humihumi.com/#/space/' + route.params.id, { width: 105, margin: 0, color: { light: '#fff', dark: '#DE1E64' } }).then(() => model.value.qrcode = true);
+});
+</script>
+
+<template>
+    <div class="card-container">
+        <div class="avatar"><img v-if="model.user.avatar" :src="url(model.user.avatar)" /></div>
+        <div class="card-content">
+            <div class="user-qrcode">
+                <div class="user-content">
+                    <div class="user-name">
+                        <span>{{ model.user.nick }}</span>
+                    </div>
+                    <div class="text-num">
+                        <div class="text">
+                            <span>{{ message(route.params.lang, 'follow.fan') }}</span>
+                        </div>
+                        <div class="num">
+                            <span>{{ model.fans.influencer }}</span>
+                        </div>
+                    </div>
+                </div>
+                <div class="qrcode">
+                    <canvas ref="qrcode"></canvas>
+                </div>
+            </div>
+            <span class="share-time">{{ message(route.params.lang, 'share.share_at') }} {{ minute() }}</span>
+        </div>
+    </div>
+    <div v-if="model.user.id && model.fan && model.qrcode" id="done"></div>
+</template>
+
+<style lang="scss" scoped>
+.card-container {
+    background: url(../../assets/img/space-card.png) no-repeat center;
+    width: 100vw;
+    height: 50vh;
+    @include flexcc;
+    flex-direction: column;
+    position: relative;
+
+    .avatar {
+        position: relative;
+        right: 5rem;
+        top: 1.6rem;
+        width: 5.25rem;
+        height: 5.25rem;
+        border-radius: 50%;
+        background: #B2CDDA;
+        background-image: url('../../assets/img/humi2.png');
+        background-size: cover;
+        background-position: center;
+        background-repeat: no-repeat;
+        overflow: hidden;
+        border: 4px solid #fff;
+
+        img {
+            width: 100%;
+            height: 100%;
+        }
+    }
+
+    .card-content {
+        width: 280px;
+        height: 120px;
+        display: flex;
+        flex-direction: column;
+        justify-content: space-between;
+        position: relative;
+        top: 1.6rem;
+
+        .user-qrcode {
+            @include flexbs;
+
+
+            .user-content {
+                padding-left: .625rem;
+                padding-top: .3125rem;
+                @include flexbs;
+                flex-direction: column;
+                row-gap: .225rem;
+
+                .user-name {
+                    position: relative;
+                    font-family: PingFang SC-Heavy;
+                    font-size: 1.225rem;
+                    line-height: 1.1725rem;
+                    color: #000;
+                }
+
+                .text-num {
+                    @include flexxl;
+
+                    .text {
+                        font-family: Source Han Sans;
+                        font-weight: bold;
+                        font-size: .85rem;
+                        padding-right: .1875rem;
+                    }
+
+                    .num {
+                        font-family: Poppins;
+                        font-weight: bold;
+                        color: #DE1E64;
+                        font-size: 1.1rem;
+                    }
+                }
+            }
+
+            .qrcode {
+                max-width: 105px;
+                max-height: 105px;
+
+                canvas {
+                    width: 100%;
+                    height: 100%;
+                }
+            }
+        }
+
+        .share-time {
+            padding-right: 5px;
+            color: #C4C4C4;
+            font-size: .6rem;
+            white-space: nowrap;
+            text-align: end;
+            font-family: PingFang SC-Medium;
+        }
+    }
+}
+</style>

+ 25 - 0
vite.config.js

@@ -0,0 +1,25 @@
+import { fileURLToPath, URL } from 'node:url'
+
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+  plugins: [
+    vue(),
+  ],
+  base: './',
+  resolve: {
+    alias: {
+      '@': fileURLToPath(new URL('./src', import.meta.url))
+    }
+  },
+  css: {
+    preprocessorOptions: {
+      scss: {
+        charest: false,
+        additionalData: `@import "@/assets/scss/variable.scss";`,
+      },
+    },
+  },
+})