Browse Source

feat:init

TsingfunLee 7 months ago
commit
d9960078f1
8 changed files with 833 additions and 0 deletions
  1. 563 0
      App.vue
  2. 2 0
      assets/hero-glow.svg
  3. BIN
      assets/logo.png
  4. 23 0
      assets/main.css
  5. 82 0
      util/http.js
  6. 7 0
      util/keyvalue.js
  7. 150 0
      util/locale.js
  8. 6 0
      util/store.js

+ 563 - 0
App.vue

@@ -0,0 +1,563 @@
+<script setup>
+import { ref, onMounted } from 'vue';
+import { service } from './util/http';
+import { message, getLang } from './util/locale';
+import { store } from './util/store';
+
+const model = ref({
+  type: '',
+  forward: '',
+  index: 0,
+  speak: 0,
+  welcome: [],
+  inputs: [],
+  focus: {
+    index: 0,
+    auto: true,
+  },
+  submit: false,
+  invalid: '',
+  forget: false,
+  interval: 0,
+});
+const inputs = { 'in': ['username', 'password'], 'up': ['username', 'nick', 'password', 'repeat'], 'forget': ['username', 'email', 'code', 'newpass', 'repeat'] };
+
+const language = (lang) => {
+    store.language = lang;
+    localStorage.setItem('language', lang);
+    window.location.reload();
+};
+
+const type = (i, j) => {
+  if (model.value.interval)
+    clearInterval(model.value.interval);
+  if ((i === 0 && j === 2) || (i === 1 && j === 1)) {
+    model.value = {
+      type: i === 1 && j === 1 ? 'forget' : (model.value.type === 'up' || model.value.type === 'forget' ? 'in' : 'up'),
+      forward: model.value.forward,
+      index: 0,
+      speak: 0,
+      welcome: [],
+      inputs: [],
+      focus: {
+        index: 0,
+        auto: true,
+      },
+      submit: false,
+      invalid: '',
+      forget: false,
+    };
+    model.value.interval = setInterval(next, model.value.type === 'in' ? 50 : 100);
+  }
+};
+
+const next = () => {
+  if (model.value.submit || speak())
+    return;
+
+  model.value.index++;
+  model.value.speak = 0;
+};
+
+const speak = () => {
+  if (model.value.index === 0) {
+    let welcome = message('welcome.' + model.value.type);
+    if (model.value.speak > welcome.length)
+      return false;
+
+    model.value.welcome = welcome.substring(0, model.value.speak++).split('\n');
+
+    return true;
+  }
+
+  let index = model.value.index - 1;
+  if (model.value.focus.auto)
+    model.value.focus.index = index;
+  if (index < inputs[model.value.type].length) {
+    let label = message(inputs[model.value.type][index]);
+    if (index === 0) {
+      label = message('username.' + model.value.type);
+    } else if (index === 1 && model.value.type === 'in')
+      label += '\n' + message('forget');
+    if (model.value.speak > label.length) {
+      if (model.value.inputs[index].step === 0)
+        model.value.inputs[index].step = 1;
+
+      return model.value.inputs[index].step < 3;
+    }
+
+    if (model.value.inputs.length < model.value.index) {
+      let input = {
+        label: [],
+        value: '',
+        placeholder: '',
+        invalid: '',
+        step: 0,
+        password: index >= (model.value.type === 'up' ? 2 : (model.value.type === 'in' ? 1 : 3)),
+      };
+      if (index === 0) {
+        if (model.value.type === 'in')
+          input.placeholder = message('username-email');
+        else if (model.value.type === 'up')
+          input.placeholder = message('username.placeholder');
+      }
+      model.value.inputs.push(input);
+    }
+    model.value.inputs[index].label = label.substring(0, model.value.speak++).split('\n');
+
+    return true;
+  }
+
+  return true;
+};
+
+const input = (index) => {
+  let value = model.value.inputs[index].value;
+  length = value.length;
+  if (index === 0) {
+    if (model.value.type === 'up') {
+      model.value.inputs[index].invalid = length < 4 || length > 15 ? message('username.invalid.up') : '';
+      if (!model.value.inputs[index].invalid) {
+        service('/user/auth/exists', { uid: value }, data => {
+          model.value.inputs[index].step = data ? 2 : 1;
+          model.value.inputs[index].invalid = data ? '' : message('username.invalid.up');
+        });
+      }
+    } else {
+      model.value.inputs[index].invalid = '';
+      model.value.inputs[index].step = 2;
+    }
+  } else if (index === 1) {
+    if (model.value.type === 'up') {
+      model.value.inputs[index].invalid = length < 2 || length > 14 ? message('nick.invalid') : '';
+      model.value.inputs[index].step = length < 2 || length > 14 ? 1 : 2;
+    } else if (model.value.type === 'in') {
+      model.value.inputs[index].invalid = length < 7 || length > 20 ? message('password.invalid') : '';
+      model.value.inputs[index].step = length < 7 || length > 20 ? 1 : 2;
+    } else if (model.value.type === 'forget') {
+      let valid = /^(?:\w+\.?-?)*\w+@(?:\w+\.?-?)*\w+$/.test(value);
+      model.value.inputs[index].invalid = !valid ? message('email.invalid') : '';
+      model.value.inputs[index].step = !valid ? 1 : 2;
+    }
+  } else if (index === 2) {
+    if (model.value.type === 'up') {
+      model.value.inputs[index].invalid = length < 7 || length > 20 ? message('password.invalid') : '';
+      model.value.inputs[index].step = length < 7 || length > 20 ? 1 : 2;
+    } else if (model.value.type === 'forget') {
+      model.value.inputs[index].invalid = length === 8 ? '' : message('code.invalid');
+      model.value.inputs[index].step = length === 8 ? 2 : 1;
+    }
+  } else if (index === 3) {
+    if (model.value.type === 'up') {
+      model.value.inputs[index].invalid = value === model.value.inputs[index - 1].value ? '' : message('repeat.invalid');
+      model.value.inputs[index].step = value === model.value.inputs[index - 1].value ? 2 : 1;
+    } else if (model.value.type === 'forget') {
+      model.value.inputs[index].invalid = length < 7 || length > 20 ? message('password.invalid') : '';
+      model.value.inputs[index].step = length < 7 || length > 20 ? 1 : 2;
+    }
+  } else if (index === 4) {
+    model.value.inputs[index].invalid = value === model.value.inputs[index - 1].value ? '' : message('repeat.invalid');
+    model.value.inputs[index].step = value === model.value.inputs[index - 1].value ? 2 : 1;
+  }
+};
+
+const confirm = (index, event, callback) => {
+  if (event)
+    event.target.blur();
+  if (model.value.submit) {
+    model.value.focus = { index: model.value.inputs.length + 1, auto: false };
+
+    return;
+  }
+
+  if (model.value.type === 'forget' && !callback) {
+    if (index === 1) {
+      service('/home/forget-email', { uid: model.value.inputs[0].value, email: model.value.inputs[1].value, subject: message('forget.title'), lang: getLang() }, data => {
+        model.value.invalid = message(data ? 'code.send' : 'email.failure');
+        if (data)
+          confirm(index, null, 1);
+      });
+
+      return;
+    }
+
+    if (index === 2) {
+      service('/home/forget-code', { code: model.value.inputs[2].value }, data => {
+        if (data)
+          confirm(index, null, 1);
+        model.value.invalid = data ? '' : message('code.invalid');
+      });
+
+      return;
+    }
+  }
+
+  model.value.focus.auto = true;
+  if (model.value.inputs[index].step === 2)
+    model.value.inputs[index].step = 3;
+  if (model.value.inputs[index].step === 3) {
+    model.value.submit = (index === 1 && model.value.type === 'in') || (index === 3 && model.value.type === 'up') || (index === 4 && model.value.type === 'forget');
+    if (model.value.submit)
+      model.value.focus = { index: index + 1, auto: false };
+  }
+};
+
+const focus = (index) => {
+  model.value.focus = { index, auto: false };
+};
+
+const submit = () => {
+  if (model.value.type === 'up') {
+    service('/user/sign-up', {
+      type: '',
+      uid: model.value.inputs[0].value,
+      nick: model.value.inputs[1].value,
+      password: model.value.inputs[2].value,
+    }, data => {
+      if (data.id) {
+        forward();
+      } else {
+        model.value.invalid = message('sign-up.invalid');
+      }
+    }, () => model.value.invalid = message('sign-up.invalid'));
+  } else if (model.value.type === 'in') {
+    service('/user/sign-in', {
+      type: '',
+      uid: model.value.inputs[0].value,
+      password: model.value.inputs[1].value,
+    }, data => {
+      if (data.id) {
+        forward();
+      } else {
+        model.value.invalid = message('sign-in.invalid');
+      }
+    }, () => model.value.invalid = message('sign-in.invalid'));
+  } else if (model.value.type === 'forget') {
+    service('/home/password', { code: model.value.inputs[2].value, password: model.value.inputs[3].value }, data => {
+      model.value.forget = data;
+      model.value.invalid = message('forget.' + (data ? 'success' : 'failure'));
+    });
+  }
+};
+
+const forward = () => {
+  if (model.value.forward === 'parent') {
+    window.parent.postMessage({ type: 'sign', value: '' }, '*');
+    window.parent.postMessage({ type: 'session', value: localStorage.getItem('photon-session-id')}, '*')
+  } else {
+    location.href = model.value.forward;
+  }
+};
+
+onMounted(() => {
+  document.title = message('title');
+  if (location.search && location.search.indexOf('?') > -1) {
+    for (let param of location.search.substring(1).split('&')) {
+      if (param.indexOf('type=') === 0) {
+        model.value.type = param.substring(5);
+      } else if (param.indexOf('forward=') === 0) {
+        model.value.forward = decodeURIComponent(param.substring(8));
+      }
+    }
+  }
+  if (!model.value.type)
+    model.value.type = 'in';
+  if (!model.value.forward)
+    model.value.forward = '/';
+  model.value.interval = setInterval(next, model.value.type === 'in' ? 50 : 100);
+  service('/user/sign', {}, data => {
+    if (data.id){
+      if(model.value.forward === 'parent'){
+        window.parent.postMessage({ type: 'sign', value: data.id }, '*');
+        window.parent.postMessage({ type: 'session', value: localStorage.getItem('photon-session-id')}, '*')
+      }else{
+        location.href = model.value.forward;
+      }
+    }
+  });
+});
+</script>
+
+<template>
+  <div class="stars">
+    <div v-for="i in 6" class="star"></div>
+  </div>
+  <div class="sign-in-up">
+    <div class="nav">
+      <img src="./assets/logo.png" />
+      <div class="langs">
+        <div @click="language('zh')"><span>中</span></div>
+        <div @click="language('en')"><span>En</span></div>
+        <div @click="language('jp')"><span>あ</span></div>
+      </div>
+    </div>
+    <div>
+      <div class="dialog">
+        <div v-for="welcome in model.welcome" class="welcome">{{ welcome }}</div>
+        <template v-for="(ip, index) in model.inputs">
+          <div class="label">
+            <span v-for="(label, li) in ip.label" :class="'label-' + (li === 0 ? '0' : (index + '' + li))"
+              @click="type(index, li)">{{ label }}</span>
+          </div>
+          <div v-if="ip.step > 0" class="input">
+            <div v-if="model.focus.index === index" class="arrow">→</div>
+            <div v-else-if="model.type === 'up'" class="correct">√</div>
+            <input :type="ip.password ? 'password' : 'text'" v-model="model.inputs[index].value"
+              :placeholder="model.inputs[index].placeholder" @focus="focus(index)" @input="input(index)"
+              @keypress.enter="confirm(index, $event)" />
+            <div v-if="model.focus.index === index"
+              :class="model.inputs[index].step === 2 ? 'button-valid' : 'button-invalid'" @click="confirm(index)">{{
+                message('continue') }}</div>
+            <div v-else class="button-empty">{{ message('continue') }}</div>
+          </div>
+          <div v-else-if="ip.step === 3" class="input">
+            <div v-if="model.type === 'up'" class="correct">√</div>
+            <div class="value">{{ model.inputs[index].password ? '********' : model.inputs[index].value }}</div>
+          </div>
+        </template>
+        <div v-if="model.forget" class="submit" @click="type(0, 2)">{{ message('to-in') }}</div>
+        <div v-else-if="model.submit" class="submit" @click="submit">{{ message('sign-' + model.type) }}</div>
+      </div>
+      <div v-if="model.inputs.length > 0 && model.inputs.length >= model.index" class="invalid">{{
+        model.inputs[model.index - 1].invalid }}</div>
+      <div v-if="model.invalid" class="invalid">{{ model.invalid }}</div>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.stars,
+.star {
+  position: absolute;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  overflow: hidden;
+}
+
+.stars {
+  background-color: #040d21;
+  background-image: url(./assets/hero-glow.svg);
+  background-size: cover;
+  background-position: center center;
+}
+
+.star {
+  background-image: radial-gradient(2px 2px at 50px 200px, #eee, rgba(0, 0, 0, 0)), radial-gradient(2px 2px at 40px 70px, #fff, rgba(0, 0, 0, 0)), radial-gradient(3px 4px at 120px 40px, #ddd, rgba(0, 0, 0, 0));
+  background-repeat: repeat;
+  background-size: 250px 250px;
+  opacity: 0;
+  animation: zoom 10s infinite;
+}
+
+.star:nth-child(1) {
+  background-position: 10% 90%;
+  animation-delay: 0s
+}
+
+.star:nth-child(2) {
+  background-position: 20% 50%;
+  background-size: 270px 500px;
+  animation-delay: .3s
+}
+
+.star:nth-child(3) {
+  background-position: 40% -80%;
+  animation-delay: 1.2s
+}
+
+.star:nth-child(4) {
+  background-position: -20% -30%;
+  transform: rotate(60deg);
+  animation-delay: 2.5s
+}
+
+.star:nth-child(5) {
+  background-image: radial-gradient(2px 2px at 10px 100px, #eee, rgba(0, 0, 0, 0)), radial-gradient(2px 2px at 20px 10px, #fff, rgba(0, 0, 0, 0)), radial-gradient(3px 4px at 150px 40px, #ddd, rgba(0, 0, 0, 0));
+  background-position: 80% 30%;
+  animation-delay: 4s
+}
+
+.star:nth-child(6) {
+  background-position: 50% 20%;
+  animation-delay: 6s
+}
+
+@keyframes zoom {
+  0% {
+    opacity: 0;
+    transform: scale(0.5);
+    transform: rotate(5deg);
+    animation-timing-function: ease-in
+  }
+
+  85% {
+    opacity: 1
+  }
+
+  100% {
+    opacity: .2;
+    transform: scale(2.2)
+  }
+}
+
+@media(prefers-reduced-motion) {
+  .star {
+    animation: none
+  }
+}
+
+.sign-in-up {
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+@media screen and (max-width: 1080px) {
+  .sign-in-up {
+    left: 0;
+    right: 0;
+  }
+}
+
+@media screen and (min-width: 1080px) {
+  .sign-in-up {
+    left: 20vw;
+    right: 20vw;
+  }
+}
+
+.nav {
+  width: calc(100% - 32px);
+  padding: 16px;
+  display: flex;
+  justify-content: space-between;
+}
+
+.nav img {
+  width: 32px;
+  height: 32px;
+}
+
+.nav .langs{
+  color:#8193b2;
+  display: flex;
+  column-gap: 8px;
+  cursor: pointer;
+}
+
+.dialog {
+  margin: 24px;
+  padding: 24px;
+  background-color: #0c162d;
+  border: 1px solid #202637;
+  border-radius: 6px;
+}
+
+.welcome {
+  width: 33vw;
+  min-width: 280px;
+  color: #8193b2;
+  text-align: center;
+}
+
+.label {
+  margin-top: 24px;
+  font-weight: 600;
+  display: flex;
+  align-items: center;
+}
+
+.label-0 {
+  color: #00cfc8;
+}
+
+.label-01 {
+  padding-left: 16px;
+  color: #8193b2;
+}
+
+.label-02,
+.label-11 {
+  color: #FF8DC0;
+  cursor: pointer;
+}
+
+.label-11 {
+  padding-left: 16px;
+}
+
+.input {
+  display: flex;
+  align-items: center;
+  padding-top: 4px;
+}
+
+.arrow {
+  color: #ea4aaa;
+}
+
+.correct {
+  color: #20bb3d;
+  padding-right: 8px;
+}
+
+.input input {
+  flex-grow: 1;
+  outline: none;
+  border: 1px solid #0c162d;
+  background: none;
+  color: #fff;
+}
+
+.input input:focus {
+  border: 1px solid rgb(9, 105, 218);
+}
+
+.value {
+  color: #fff;
+}
+
+.button-invalid,
+.button-valid,
+.button-empty {
+  padding: 4px 8px;
+  border-radius: 6px;
+  white-space: nowrap;
+}
+
+.button-invalid {
+  color: #627597;
+  border: 1px solid #627597;
+  cursor: default;
+}
+
+.button-valid,
+.submit {
+  color: #FF8DC0;
+  border: 1px solid #FF8DC0;
+  cursor: pointer;
+}
+
+.button-empty {
+  color: #0c162d;
+  border: 1px solid #0c162d;
+  cursor: default;
+}
+
+.submit {
+  text-align: center;
+  padding: 8px 0;
+  border-radius: 6px;
+  margin-top: 24px;
+}
+
+.invalid {
+  padding: 0 48px;
+  color: #8193b2;
+}
+</style>

+ 2 - 0
assets/hero-glow.svg

@@ -0,0 +1,2 @@
+
+<svg fill="none" height="1602" viewBox="0 0 2769 1602" width="2769" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><filter id="a" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse" height="1044" width="1044" x="1682" y="558"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" mode="normal" result="shape"/><feGaussianBlur result="effect1_foregroundBlur" stdDeviation="100"/></filter><filter id="b" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse" height="1044" width="1044" x="1725" y="0"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" mode="normal" result="shape"/><feGaussianBlur result="effect1_foregroundBlur" stdDeviation="100"/></filter><filter id="c" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse" height="858" width="982" x="0" y="23"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" mode="normal" result="shape"/><feGaussianBlur result="effect1_foregroundBlur" stdDeviation="100"/></filter><filter id="d" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse" height="646" width="982" x="279" y="58"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" mode="normal" result="shape"/><feGaussianBlur result="effect1_foregroundBlur" stdDeviation="100"/></filter><g filter="url(#a)" opacity=".34"><circle cx="2204" cy="1080" fill="#db469f" r="322"/></g><g filter="url(#b)" opacity=".7"><circle cx="2247" cy="522" fill="#043a8a" r="322"/></g><g filter="url(#c)" opacity=".9"><ellipse cx="491" cy="452" fill="#043a8a" rx="291" ry="229"/></g><g filter="url(#d)" opacity=".43"><ellipse cx="770" cy="381" fill="#043a8a" rx="291" ry="123"/></g></svg>

BIN
assets/logo.png


+ 23 - 0
assets/main.css

@@ -0,0 +1,23 @@
+html,
+body {
+    width: 100vw;
+    height: 100vh;
+    margin: 0;
+    padding: 0;
+    overflow: hidden;
+}
+
+#app {
+    width: 100%;
+    height: 100%;
+    overflow: auto;
+}
+
+img,
+svg {
+    display: block;
+}
+
+* {
+    font-size: 16px;
+}

+ 82 - 0
util/http.js

@@ -0,0 +1,82 @@
+const root = import.meta.env.PROD ? '': '/api';
+
+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, fileName, 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();
+    if (fileName)
+        body.append(name, file, fileName);
+    else
+        body.append(name, file);
+    xhr.send(body);
+}
+
+const psid = (header) => {
+    // localStorage.setItem('photon-session-id', 'bh4n8magnjwix9q0k51utrdy0uk9gpuqfp4ssn0z8xdna0nlaq09xdugawveuq3u'); // 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,
+}

+ 7 - 0
util/keyvalue.js

@@ -0,0 +1,7 @@
+import { service } from './http';
+
+const keyvalue = (key, success) => service('/keyvalue/object', { key }, success);
+
+export {
+    keyvalue
+}

+ 150 - 0
util/locale.js

@@ -0,0 +1,150 @@
+import { store } from './store';
+
+const messages = {
+    "en": {
+        "code": "Please enter the verification code",
+        "code.invalid": "Invalid verification code",
+        "code.send": "The verification code has been sent",
+        "continue": "Continue",
+        "email": "Please enter your email",
+        "email.failure": "Account and email do not match",
+        "email.invalid": "Please enter a valid email address",
+        "forget": "Forgot password?",
+        "forget.failure": "Failed to reset the password",
+        "forget.success": "Your password has been successfully reset. You can now use the new password to log in",
+        "forget.title": "Your HumiHumi Verification Code",
+        "newpass": "Please enter your new password",
+        "nick": "Nickname",
+        "nick.invalid": "Invalid nickname",
+        "nick.placeholder": "2~14 characters",
+        "password": "Enter password",
+        "password.invalid": "Password must be 7 ~ 20 characters long",
+        "password.placeholder": "7~20 characters",
+        "repeat": "Please re-enter your password",
+        "repeat.invalid": "The passwords entered do not match",
+        "sign-forget": "Set a new password",
+        "sign-in": "Log In",
+        "sign-in.invalid": "Invalid username or password, please try again",
+        "sign-up": "Create Account",
+        "sign-up.invalid": "Failed to create an account, please try again",
+        "title": "HumiHumi",
+        "to-in": "Log In",
+        "to-up": "Create one",
+        "username": "Please enter your username",
+        "username-email": "Username/Email",
+        "username.forget": "Enter username",
+        "username.in": "Enter username\n\nCreate one",
+        "username.invalid.in": "Invalid username",
+        "username.invalid.up": "Invalid username or already taken by someone else",
+        "username.placeholder": "5~14 characters",
+        "username.up": "Create username\n\nLog in",
+        "welcome.forget": "ฅ●ω●ฅ Welcome to\nHumi Kingdom\nLet's change the password~",
+        "welcome.in": "ฅ●ω●ฅ Welcome back to\nHumi Kingdom\nLet's continue exploring~",
+        "welcome.up": "ฅ●ω●ฅ Welcome to\nHumi Kingdom\nLet's start exploring~"
+    },
+    "jp": {
+        "code": "認証コードを入力して下さい",
+        "code.invalid": "認証コードが間違っています",
+        "code.send": "認証コードが紐付けしたメールアドレスに送信いたしました",
+        "continue": "続ける",
+        "email": "メールアドレスを入力して下さい",
+        "email.failure": "メールアドレスが間違っています",
+        "email.invalid": "紐付けしたメールアドレスを入力して下さい",
+        "forget": "忘れた?",
+        "forget.failure": "パスワードをリセットできません",
+        "forget.success": "パスワードをリセットしました",
+        "forget.title": "HumiHumiの認証コードです",
+        "newpass": "パスワードを入力",
+        "nick": "ニックネームを入力",
+        "nick.invalid": "ニックネームが無効です",
+        "nick.placeholder": "2~14文字",
+        "password": "パスワードを入力",
+        "password.invalid": "パスワードは7文字以上20文字以下で入力してください",
+        "password.placeholder": "7~20文字",
+        "repeat": "パスワードを再入力",
+        "repeat.invalid": "再度入力したパスワードが設定したパスワードと一致しません",
+        "sign-forget": "新たなパスワードを設置する",
+        "sign-in": "ログイン",
+        "sign-in.invalid": "ユーザー名またはパスワードが間違っています、もう一度お試しください",
+        "sign-up": "アカウントを作成する",
+        "sign-up.invalid": "アカウントの作成に失敗しました、もう一度お試しください",
+        "title": "HumiHumi",
+        "to-in": "ログイン",
+        "to-up": "登録",
+        "username": "アカウントを入力",
+        "username-email": "ユーザ名/E-mail",
+        "username.forget": "ユーザー名を入力",
+        "username.in": "ユーザー名を入力\n持ってない?\n登録",
+        "username.invalid.in": "ユーザー名が無効です",
+        "username.invalid.up": "ユーザー名が無効、または既に他の人に使用されています",
+        "username.placeholder": "5~14文字",
+        "username.up": "ユーザー名を作る\n持ってる?\nログイン",
+        "welcome.forget": "ฅ●ω●ฅようこそHumi王国へ\nパスワードをリセットしましょう~",
+        "welcome.in": "ฅ●ω●ฅHumi王国へお帰りなさい\n探索を続けましょう",
+        "welcome.up": "ฅ●ω●ฅHumi王国へようこそ\n探索を始めましょう"
+    },
+    "zh": {
+        "code": "请输入验证码",
+        "code.invalid": "无效的验证码",
+        "code.send": "验证码已发送到绑定的邮箱",
+        "continue": "继续",
+        "email": "请输入绑定的Email",
+        "email.failure": "账号与Email不匹配",
+        "email.invalid": "请输入有效的Email地址",
+        "forget": "忘记密码?",
+        "forget.failure": "密码重置失败",
+        "forget.success": "密码已重置,您可以使用新密码登录了",
+        "forget.title": "您的HumiHumi验证码",
+        "newpass": "请输入您的新密码",
+        "nick": "请输入您的昵称",
+        "nick.invalid": "昵称无效",
+        "nick.placeholder": "2~14个字符",
+        "password": "请输入您的密码",
+        "password.invalid": "密码长度需要7~20个字符",
+        "password.placeholder": "7~20个字符",
+        "repeat": "请再次输入您的密码",
+        "repeat.invalid": "再次输入的密码与您设置的密码不一致",
+        "sign-forget": "设置新密码",
+        "sign-in": "登录",
+        "sign-in.invalid": "用户名或密码错误,请重试",
+        "sign-up": "创建账号",
+        "sign-up.invalid": "账号创建失败,请重试",
+        "title": "Humi虎咪",
+        "to-in": "登录",
+        "to-up": "注册",
+        "username": "请输入您的账号",
+        "username-email": "用户名/邮箱",
+        "username.forget": "请输入您的账号",
+        "username.in": "请输入您的账号\n还没有账号?\n注册",
+        "username.invalid.in": "用户名无效",
+        "username.invalid.up": "用户名无效或已被其他人使用",
+        "username.placeholder": "5~14个字符",
+        "username.up": "请创建您的账号\n已有账号?\n登录",
+        "welcome.forget": "ฅ●ω●ฅ欢迎来到Humi王国\n让我们修改密码吧~",
+        "welcome.in": "ฅ●ω●ฅ欢迎回到Humi王国\n让我们继续探索吧~",
+        "welcome.up": "ฅ●ω●ฅ欢迎来到Humi王国\n让我们开始探索吧~"
+    }
+};
+
+const message = (key) => {
+    return messages[getLang()][key] || '';
+}
+
+const getLang = () => {
+    let language = localStorage.getItem('language') || 'sys';
+    if (language === 'sys') {
+        language = navigator.language;
+        let indexOf = language.indexOf('-');
+        if (indexOf > -1)
+            language = language.substring(0, indexOf);
+        if (!messages[language])
+            language = 'en';
+    }
+
+    return language;
+};
+
+export {
+    message,
+    getLang,
+}

+ 6 - 0
util/store.js

@@ -0,0 +1,6 @@
+import { reactive } from "vue";
+
+export const store = reactive({
+    user: {},
+    language: localStorage.getItem('language'),
+});