unknown 1 ヶ月 前
親
コミット
ff3af68a2c

+ 1 - 0
fastadmin/application/admin/controller/ps/TemplateVersion.php

@@ -22,6 +22,7 @@ class TemplateVersion extends Backend
     {
         parent::_initialize();
         $this->model = new \app\admin\model\ps\Template_version;
+        $this->searchFields = 'id,version,remark';
 
     }
 

+ 9 - 4
fastadmin/application/admin/view/ps/template/edit.html

@@ -3,7 +3,12 @@
     <div class="form-group">
         <label class="control-label col-xs-12 col-sm-2">{:__('Factory_id')}:</label>
         <div class="col-xs-12 col-sm-8">
-            <input id="c-factory_id" data-rule="required" min="0" data-source="ps/factory/index" data-field="name" class="form-control selectpage" name="row[factory_id]" type="text" value="{$row.factory_id|htmlentities}">
+            <select id="c-factory_id" class="form-control" name="row[factory_id]">
+                <option value="0"{if condition="$row.factory_id eq 0"} selected{/if}>官方(所有工厂可套打)</option>
+                {foreach name="factories" item="fname" key="fid"}
+                <option value="{$fid}"{if condition="$row.factory_id eq $fid"} selected{/if}>{$fname}</option>
+                {/foreach}
+            </select>
         </div>
     </div>
     <div class="form-group">
@@ -67,19 +72,19 @@
     <div class="form-group">
         <label class="control-label col-xs-12 col-sm-2">{:__('Source_template_id')}:</label>
         <div class="col-xs-12 col-sm-8">
-            <input id="c-source_template_id" data-rule="required" min="0" data-source="source/template/index" class="form-control selectpage" name="row[source_template_id]" type="text" value="{$row.source_template_id|htmlentities}">
+            <input id="c-source_template_id" min="0" data-source="ps/template/index" data-field="name" class="form-control selectpage" name="row[source_template_id]" type="text" value="{$row.source_template_id|htmlentities}">
         </div>
     </div>
     <div class="form-group">
         <label class="control-label col-xs-12 col-sm-2">{:__('Source_version_id')}:</label>
         <div class="col-xs-12 col-sm-8">
-            <input id="c-source_version_id" data-rule="required" min="0" data-source="source/version/index" class="form-control selectpage" name="row[source_version_id]" type="text" value="{$row.source_version_id|htmlentities}">
+            <input id="c-source_version_id" min="0" data-source="ps/template_version/index" data-field="id" class="form-control selectpage" name="row[source_version_id]" type="text" value="{$row.source_version_id|htmlentities}">
         </div>
     </div>
     <div class="form-group">
         <label class="control-label col-xs-12 col-sm-2">{:__('Current_version_id')}:</label>
         <div class="col-xs-12 col-sm-8">
-            <input id="c-current_version_id" data-rule="required" min="0" data-source="current/version/index" class="form-control selectpage" name="row[current_version_id]" type="text" value="{$row.current_version_id|htmlentities}">
+            <input id="c-current_version_id" min="0" data-source="ps/template_version/index" data-field="id" class="form-control selectpage" name="row[current_version_id]" type="text" value="{$row.current_version_id|htmlentities}">
         </div>
     </div>
     <div class="form-group">

+ 9 - 3
fastadmin/application/api/controller/User.php

@@ -110,6 +110,7 @@ class User extends Api
      * @ApiParams (name="mobile", type="string", required=true, description="手机号")
      * @ApiParams (name="code", type="string", required=true, description="验证码")
      * @ApiParams (name="invite_code", type="string", required=false, description="工厂邀请码,填写则加入该厂成为操作员")
+     * @ApiParams (name="factory_name", type="string", required=false, description="注册开厂时必填的工厂名称")
      */
     public function register()
     {
@@ -120,9 +121,13 @@ class User extends Api
         $code = $this->request->post('code');
         $nickname = $this->request->post('nickname', '');
         $inviteCode = $this->request->post('invite_code', '');
+        $factoryName = trim((string)$this->request->post('factory_name', ''));
         if (!$username || !$password) {
             $this->error(__('Invalid parameters'));
         }
+        if (trim((string)$inviteCode) === '' && $factoryName === '') {
+            $this->error('请填写工厂名称');
+        }
         if ($email && !Validate::is($email, "email")) {
             $this->error(__('Email is incorrect'));
         }
@@ -145,7 +150,7 @@ class User extends Api
         }
         $ret = $this->auth->register($username, $password, $email, $mobile, $extend);
         if ($ret) {
-            $this->bindFactoryIfNeeded($inviteCode);
+            $this->bindFactoryIfNeeded($inviteCode, $factoryName);
             $data = ['userinfo' => $this->auth->getUserinfo()];
             $this->success(__('Sign up successful'), $data);
         } else {
@@ -171,15 +176,16 @@ class User extends Api
     /**
      * 登录/注册后补绑工厂。无邀请码则创建本厂。
      * @param string $inviteCode
+     * @param string $factoryName
      */
-    protected function bindFactoryIfNeeded($inviteCode = '')
+    protected function bindFactoryIfNeeded($inviteCode = '', $factoryName = '')
     {
         $user = $this->auth->getUser();
         if (!$user) {
             return;
         }
         try {
-            FactoryModel::ensureBound($user, $inviteCode);
+            FactoryModel::ensureBound($user, $inviteCode, $factoryName);
         } catch (\Exception $e) {
             $this->error($e->getMessage());
         }

+ 12 - 3
fastadmin/application/common/behavior/PsFactory.php

@@ -30,12 +30,21 @@ class PsFactory
             return;
         }
         $invite = '';
+        $factoryName = '';
         if (is_array($data) && isset($data['invite_code'])) {
             $invite = trim($data['invite_code']);
         }
-        if ($invite === '' && request()) {
-            $invite = (string)request()->post('invite_code', '');
+        if (is_array($data) && isset($data['factory_name'])) {
+            $factoryName = trim((string)$data['factory_name']);
         }
-        Factory::ensureBound($user, $invite);
+        if (request()) {
+            if ($invite === '') {
+                $invite = (string)request()->post('invite_code', '');
+            }
+            if ($factoryName === '') {
+                $factoryName = trim((string)request()->post('factory_name', ''));
+            }
+        }
+        Factory::ensureBound($user, $invite, $factoryName);
     }
 }

+ 15 - 6
fastadmin/application/common/model/ps/Factory.php

@@ -23,10 +23,11 @@ class Factory extends Model
      * ThinkPHP 5 的 Config::get 只支持两级键,不能用 ps.factory.register_create_factory。
      * @param User   $user
      * @param string $inviteCode
+     * @param string $factoryName 注册开厂时必填,不用昵称拼「某某的工厂」
      * @return Factory|null
      * @throws Exception
      */
-    public static function ensureBound(User $user, $inviteCode = '')
+    public static function ensureBound(User $user, $inviteCode = '', $factoryName = '')
     {
         if ((int)$user->factory_id > 0) {
             return self::get($user->factory_id);
@@ -43,24 +44,32 @@ class Factory extends Model
         if (!$autoCreate) {
             return null;
         }
-        return self::createForOwner($user);
+        return self::createForOwner($user, $factoryName);
     }
 
     /**
      * 为新注册用户创建工厂并设为管理员
-     * @param User $user
+     * @param User   $user
+     * @param string $factoryName
      * @return Factory
+     * @throws Exception
      */
-    public static function createForOwner(User $user)
+    public static function createForOwner(User $user, $factoryName = '')
     {
-        $name = $user->nickname ?: $user->username;
+        $name = trim((string)$factoryName);
+        if ($name === '') {
+            throw new Exception('请填写工厂名称');
+        }
+        if (mb_strlen($name) > 100) {
+            throw new Exception('工厂名称不能超过100个字');
+        }
         $scoreCfg = \think\Config::get('ps.score') ?: [];
         $gift = isset($scoreCfg['register_gift']) ? (int)$scoreCfg['register_gift'] : 0;
         if ($gift < 0) {
             $gift = 0;
         }
         $factory = self::create([
-            'name'          => $name . '的工厂',
+            'name'          => $name,
             'code'          => 'F' . date('ymd') . $user->id,
             'invite_code'   => self::uniqueInviteCode(),
             'owner_user_id' => $user->id,

+ 1 - 1
vue/index.html

@@ -6,7 +6,7 @@
     <meta name="renderer" content="webkit" />
     <meta
       name="viewport"
-      content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0"
+      content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0,viewport-fit=cover"
     />
     <title>生产流程单 · 工厂端</title>
     <link rel="icon" href="/favicon.ico" />

+ 1 - 1
vue/src/views/login/index.vue

@@ -177,7 +177,7 @@ useEventListener(document, "keydown", ({ code }) => {
                   加入工厂
                 </el-button>
               </div>
-              <p class="auth-extra-hint">注册工厂会创建本厂并成为管理员;加入工厂请向管理员要邀请码。</p>
+              <p class="auth-extra-hint">注册工厂需填写工厂名称,创建后你是管理员;加入工厂请向管理员要邀请码。</p>
             </div>
           </Motion>
         </el-form>

+ 12 - 3
vue/src/views/register/index.vue

@@ -19,9 +19,12 @@
         </div>
         <h2>{{ isJoin ? "加入已有工厂" : "注册并创建工厂" }}</h2>
         <p class="auth-sub">
-          {{ isJoin ? "向本厂管理员索取邀请码,注册后成为操作员。" : "不填邀请码,将创建本厂并成为管理员。" }}
+          {{ isJoin ? "向本厂管理员索取邀请码,注册后成为操作员。" : "填写本厂名称,创建后你是管理员。" }}
         </p>
         <el-form :model="form" label-position="top" size="large" @submit.prevent="onSubmit">
+          <el-form-item v-if="!isJoin" label="工厂名称" required>
+            <el-input v-model="form.factory_name" placeholder="请填写本厂真实名称" maxlength="100" />
+          </el-form-item>
           <el-form-item label="用户名" required>
             <el-input v-model="form.username" placeholder="用于登录的账号" />
           </el-form-item>
@@ -66,7 +69,8 @@ const form = reactive({
   password: "",
   nickname: "",
   mobile: "",
-  invite_code: ""
+  invite_code: "",
+  factory_name: ""
 });
 const isJoin = computed(() => String(route.query.mode || "") === "join");
 
@@ -75,6 +79,10 @@ function setMode(mode) {
 }
 
 async function onSubmit() {
+  if (!isJoin.value && !String(form.factory_name || "").trim()) {
+    ElMessage.warning("请填写工厂名称");
+    return;
+  }
   if (!form.username || !form.password) {
     ElMessage.warning("请填写用户名和密码");
     return;
@@ -91,7 +99,8 @@ async function onSubmit() {
       nickname: form.nickname,
       email: "",
       mobile: form.mobile,
-      invite_code: isJoin.value ? String(form.invite_code || "").trim() : ""
+      invite_code: isJoin.value ? String(form.invite_code || "").trim() : "",
+      factory_name: isJoin.value ? "" : String(form.factory_name || "").trim()
     });
     if (!res?.success) {
       ElMessage.error("注册失败");

+ 189 - 9
vue/src/views/welcome/index.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { computed, onMounted, reactive, ref } from "vue";
+import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue";
 import { useRoute, useRouter } from "vue-router";
 import { useUserStore } from "@/store/modules/user";
 import { factoryBoard, homeBlocks } from "@/api/ps";
@@ -41,6 +41,7 @@ const mine = reactive({
 });
 const site = useSiteStore();
 const blocks = ref<HomeBlock[]>([]);
+const menuOpen = ref(false);
 const brandName = computed(() => site.title);
 const firstStepsIndex = computed(() =>
   blocks.value.findIndex(item => item.type === "steps")
@@ -66,6 +67,7 @@ const heroMetrics = computed(() => {
 });
 
 onMounted(async () => {
+  window.addEventListener("resize", onResize);
   try {
     if (loggedIn.value) {
       await store.loadFactory();
@@ -88,7 +90,31 @@ function formatNum(n: number) {
   return String(v);
 }
 
+function closeMenu() {
+  menuOpen.value = false;
+}
+
+function toggleMenu() {
+  menuOpen.value = !menuOpen.value;
+}
+
+watch(menuOpen, open => {
+  document.body.style.overflow = open ? "hidden" : "";
+});
+
+function onResize() {
+  if (window.innerWidth > 900) {
+    closeMenu();
+  }
+}
+
+onBeforeUnmount(() => {
+  document.body.style.overflow = "";
+  window.removeEventListener("resize", onResize);
+});
+
 function go(path: string) {
+  closeMenu();
   if (!loggedIn.value && path !== "/login" && path !== "/register" && !path.startsWith("/register")) {
     router.push("/login");
     return;
@@ -97,6 +123,7 @@ function go(path: string) {
 }
 
 function scrollTo(id: string) {
+  closeMenu();
   document.getElementById(id)?.scrollIntoView({ behavior: "smooth", block: "start" });
 }
 
@@ -139,7 +166,7 @@ function isAnchor(path?: string) {
 
 <template>
   <div class="home" :class="{ embedded: !isPublic, public: isPublic }">
-    <header v-if="isPublic" class="pub-nav">
+    <header v-if="isPublic" class="pub-nav" :class="{ open: menuOpen }">
       <div class="wrap pub-nav-inner">
         <div class="pub-brand">{{ brandName }}</div>
         <nav class="pub-links">
@@ -165,6 +192,47 @@ function isAnchor(path?: string) {
           <button type="button" class="btn-ghost" @click="go('/register')">注册工厂</button>
           <button type="button" class="btn-solid" @click="go('/register?mode=join')">加入工厂</button>
         </div>
+        <div class="pub-mobile">
+          <button type="button" class="btn-text" @click="go('/login')">登录</button>
+          <button
+            type="button"
+            class="pub-menu-btn"
+            :aria-expanded="menuOpen"
+            :aria-label="menuOpen ? '关闭菜单' : '打开菜单'"
+            @click="toggleMenu"
+          >
+            <span />
+            <span />
+            <span />
+          </button>
+        </div>
+      </div>
+      <div v-show="menuOpen" class="pub-mask" @click="closeMenu" />
+      <div class="pub-sheet" :class="{ open: menuOpen }">
+        <div class="wrap pub-sheet-inner">
+          <nav class="pub-sheet-links">
+            <button
+              v-if="firstStepsIndex >= 0"
+              type="button"
+              class="pub-sheet-link"
+              @click="scrollTo('home-flow')"
+            >
+              怎么用
+            </button>
+            <button
+              v-if="firstFeaturesIndex >= 0"
+              type="button"
+              class="pub-sheet-link"
+              @click="scrollTo('home-features')"
+            >
+              能做什么
+            </button>
+          </nav>
+          <div class="pub-sheet-actions">
+            <button type="button" class="btn-ghost" @click="go('/register')">注册工厂</button>
+            <button type="button" class="btn-solid" @click="go('/register?mode=join')">加入工厂</button>
+          </div>
+        </div>
       </div>
     </header>
 
@@ -340,6 +408,7 @@ function isAnchor(path?: string) {
   position: sticky;
   top: 0;
   z-index: 20;
+  padding-top: env(safe-area-inset-top);
   background: rgba(255, 255, 255, 0.92);
   backdrop-filter: blur(16px);
   border-bottom: 1px solid rgba(15, 23, 42, 0.06);
@@ -352,10 +421,14 @@ function isAnchor(path?: string) {
   min-height: 72px;
 }
 .pub-brand {
+  min-width: 0;
   font-size: 18px;
   font-weight: 700;
   letter-spacing: 0.02em;
   color: #0f172a;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
 }
 .pub-links {
   display: flex;
@@ -376,10 +449,97 @@ function isAnchor(path?: string) {
 }
 .pub-actions {
   display: flex;
-  flex-wrap: wrap;
+  flex-wrap: nowrap;
+  align-items: center;
+  gap: 10px;
+  flex-shrink: 0;
+}
+.pub-mobile {
+  display: none;
+  align-items: center;
+  gap: 4px;
+  flex-shrink: 0;
+}
+.pub-menu-btn {
+  width: 40px;
+  height: 40px;
+  padding: 0;
+  border: 0;
+  border-radius: 12px;
+  background: #f1f5f9;
+  display: inline-flex;
+  flex-direction: column;
+  justify-content: center;
   align-items: center;
+  gap: 5px;
+  cursor: pointer;
+}
+.pub-menu-btn span {
+  display: block;
+  width: 16px;
+  height: 2px;
+  border-radius: 2px;
+  background: #0f172a;
+  transition: transform 0.18s ease, opacity 0.18s ease;
+}
+.pub-nav.open .pub-menu-btn span:nth-child(1) {
+  transform: translateY(7px) rotate(45deg);
+}
+.pub-nav.open .pub-menu-btn span:nth-child(2) {
+  opacity: 0;
+}
+.pub-nav.open .pub-menu-btn span:nth-child(3) {
+  transform: translateY(-7px) rotate(-45deg);
+}
+.pub-mask {
+  position: fixed;
+  inset: 0;
+  top: calc(56px + env(safe-area-inset-top));
+  z-index: 18;
+  background: rgba(15, 23, 42, 0.28);
+}
+.pub-sheet {
+  display: none;
+  position: relative;
+  z-index: 21;
+  padding: 4px 0 max(16px, env(safe-area-inset-bottom));
+  background: #fff;
+  border-top: 1px solid #eef1f6;
+  box-shadow: 0 18px 40px rgba(15, 23, 42, 0.08);
+}
+.pub-sheet.open {
+  display: block;
+}
+.pub-sheet-inner {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  padding-top: 8px;
+  padding-bottom: 8px;
+}
+.pub-sheet-links {
+  display: flex;
+  flex-direction: column;
+}
+.pub-sheet-link {
+  border: 0;
+  background: none;
+  text-align: left;
+  padding: 14px 0;
+  color: #334155;
+  font-size: 15px;
+  cursor: pointer;
+}
+.pub-sheet-actions {
+  display: flex;
+  flex-direction: column;
   gap: 10px;
 }
+.pub-sheet-actions .btn-ghost,
+.pub-sheet-actions .btn-solid {
+  width: 100%;
+  text-align: center;
+}
 
 .btn-solid,
 .btn-ghost,
@@ -714,14 +874,26 @@ h2 {
 }
 
 @media (max-width: 900px) {
-  .pub-links {
+  .pub-links,
+  .pub-actions {
     display: none;
   }
+  .pub-mobile {
+    display: flex;
+  }
+  .pub-nav-inner {
+    min-height: 56px;
+    gap: 12px;
+  }
+  .pub-brand {
+    flex: 1;
+    font-size: 16px;
+  }
   .wrap {
-    width: min(1120px, calc(100% - 40px));
+    width: min(1120px, calc(100% - 32px));
   }
   .hero {
-    padding-top: 48px;
+    padding-top: 36px;
   }
   .hero-copy {
     padding-bottom: 36px;
@@ -736,16 +908,24 @@ h2 {
   }
 }
 
+@media (min-width: 901px) {
+  .pub-mobile,
+  .pub-mask,
+  .pub-sheet {
+    display: none !important;
+  }
+}
+
 @media (max-width: 768px) {
   .home.embedded {
     margin: -16px;
   }
-  .pub-nav-inner {
-    min-height: 64px;
-  }
   .hero-title {
     font-size: 26px;
   }
+  .brand {
+    font-size: 32px;
+  }
   .stage-row {
     grid-template-columns: minmax(0, 1fr) auto;
   }