Просмотр исходного кода

Merge pull request #2329 from visuddhinanda/development

Development
visuddhinanda 8 месяцев назад
Родитель
Сommit
6a1a16bf80

+ 90 - 39
api-v8/app/Http/Api/StudioApi.php

@@ -1,71 +1,122 @@
 <?php
+
 namespace App\Http\Api;
 
 use App\Models\UserInfo;
+use App\Models\GroupInfo;
+use App\Models\GroupMember;
+
 use Illuminate\Support\Facades\Storage;
 use Illuminate\Support\Facades\App;
 
-class StudioApi{
-    public static function getIdByName($name){
+class StudioApi
+{
+    public static function getIdByName($name)
+    {
         /**
          * 获取 uuid
          */
         //TODO 改为studio table
-        if(empty($name)){
+        if (empty($name)) {
             return false;
         }
-        $userInfo = UserInfo::where('username',$name)->first();
-        if(!$userInfo){
-            return false;
-        }else{
+        $userInfo = UserInfo::where('username', $name)->first();
+        if ($userInfo) {
+            //group
             return $userInfo->userid;
+        } else {
+            $group = GroupInfo::where('uid', $name)->first();
+            if ($group) {
+                return $group->uid;
+            } else {
+                return false;
+            }
         }
-
     }
-    public static function getById($id){
+    public static function getById($id)
+    {
         //TODO 改为studio table
-        if(empty($id)){
+        if (empty($id)) {
             return false;
         }
-        $userInfo = UserInfo::where('userid',$id)->first();
-        if(!$userInfo){
-            return false;
-        }
-        $data = [
-            'id'=>$id,
-            'nickName'=>$userInfo->nickname,
-            'realName'=>$userInfo->username,
-            'studioName'=>$userInfo->username,
-        ];
-        if(!empty($userInfo->role)){
-            $data['roles'] = json_decode($userInfo->role);
-        }
-        if($userInfo->avatar){
-            $img = str_replace('.jpg','_s.jpg',$userInfo->avatar);
-            if (App::environment('local')) {
-                $data['avatar'] = Storage::url($img);
-            }else{
-                $data['avatar'] = Storage::temporaryUrl($img, now()->addDays(6));
+        $userInfo = UserInfo::where('userid', $id)->first();
+        if ($userInfo) {
+            $data = [
+                'id' => $id,
+                'nickName' => $userInfo->nickname,
+                'realName' => $userInfo->username,
+                'studioName' => $userInfo->username,
+            ];
+            if (!empty($userInfo->role)) {
+                $data['roles'] = json_decode($userInfo->role);
+            }
+            if ($userInfo->avatar) {
+                $img = str_replace('.jpg', '_s.jpg', $userInfo->avatar);
+                if (App::environment('local')) {
+                    $data['avatar'] = Storage::url($img);
+                } else {
+                    $data['avatar'] = Storage::temporaryUrl($img, now()->addDays(6));
+                }
+            }
+            return $data;
+        } else {
+            $group = GroupInfo::where('uid', $id)->first();
+            if ($group) {
+                $data = [
+                    'id' => $id,
+                    'nickName' => $group->name,
+                    'realName' => $group->uid,
+                    'studioName' => $group->uid,
+                ];
+            } else {
+                return false;
             }
         }
-        return $data;
     }
 
-    public static function getByIntId($id){
+    public static function getByIntId($id)
+    {
         //TODO 改为studio table
-        if(empty($id)){
+        if (empty($id)) {
             return false;
         }
-        $userInfo = UserInfo::where('id',$id)->first();
-        if(!$userInfo){
+        $userInfo = UserInfo::where('id', $id)->first();
+        if (!$userInfo) {
             return false;
         }
         return [
-            'id'=>$userInfo['userid'],
-            'nickName'=>$userInfo['nickname'],
-            'realName'=>$userInfo['username'],
-            'studioName'=>$userInfo['username'],
-            'avatar'=>'',
+            'id' => $userInfo['userid'],
+            'nickName' => $userInfo['nickname'],
+            'realName' => $userInfo['username'],
+            'studioName' => $userInfo['username'],
+            'avatar' => '',
         ];
     }
+
+    public static function userCanManage($userId, $studioId)
+    {
+        if ($userId === $studioId) {
+            return true;
+        }
+        $group = GroupMember::where('user_id', $userId)
+            ->where('group_id', $studioId)
+            ->first();
+        if ($group && $group->power <= 1) {
+            return true;
+        }
+        return false;
+    }
+    public static function userCanList($userId, $studioId)
+    {
+        if ($userId === $studioId) {
+            return true;
+        }
+        $group = GroupMember::where('user_id', $userId)
+            ->where('group_id', $studioId)
+            ->first();
+        if ($group && $group->power <= 2) {
+            return true;
+        }
+        return false;
+    }
 }

+ 6 - 6
api-v8/app/Http/Controllers/ChannelController.php

@@ -69,8 +69,8 @@ class ChannelController extends Controller
                 }
                 //判断当前用户是否有指定的studio的权限
                 $studioId = StudioApi::getIdByName($request->get('name'));
-                if ($user['user_uid'] !== $studioId) {
-                    return $this->error(__('auth.failed'));
+                if (!StudioApi::userCanList($user['user_uid'], $studioId)) {
+                    return $this->error(__('auth.failed'), 403, 403);
                 }
 
                 $table = Channel::select($indexCol);
@@ -296,7 +296,7 @@ class ChannelController extends Controller
             }
             return $this->ok(["rows" => $result, "count" => $count]);
         } else {
-            return $this->error("没有查询到数据");
+            return $this->ok(["rows" => [], "count" => 0]);
         }
     }
 
@@ -543,13 +543,13 @@ class ChannelController extends Controller
         }
         //判断当前用户是否有指定的studio的权限
         $studioId = StudioApi::getIdByName($request->get('studio'));
-        if ($user['user_uid'] !== $studioId) {
+        if (!StudioApi::userCanManage($user['user_uid'], $studioId)) {
             return $this->error(__('auth.failed'), 403, 403);
         }
         $studio = StudioApi::getById($studioId);
         //查询是否重复
         if (Channel::where('name', $request->get('name'))
-            ->where('owner_uid', $user['user_uid'])
+            ->where('owner_uid', $studioId)
             ->exists()
         ) {
             return $this->error(__('validation.exists', ['name']), 200, 200);
@@ -558,7 +558,7 @@ class ChannelController extends Controller
         $channel = new Channel;
         $channel->id = app('snowflake')->id();
         $channel->name = $request->get('name');
-        $channel->owner_uid = $user['user_uid'];
+        $channel->owner_uid = $studioId;
         $channel->type = $request->get('type');
         $channel->lang = $request->get('lang');
         $channel->editor_id = $user['user_id'];

+ 1 - 1
dashboard-v4/dashboard/src/components/channel/ChannelTable.tsx

@@ -246,7 +246,7 @@ const ChannelTableWidget = ({
           },
           {
             title: intl.formatMessage({
-              id: "dict.fields.createdAt.label",
+              id: "forms.fields.created-at.label",
             }),
             key: "progress",
             hideInTable: typeof chapter === "undefined",

+ 6 - 15
dashboard-v4/dashboard/src/components/template/SentEdit.tsx

@@ -1,4 +1,4 @@
-import { Affix, Card } from "antd";
+import { Affix } from "antd";
 import { useEffect, useRef, useState } from "react";
 import { IStudio } from "../auth/Studio";
 
@@ -14,6 +14,7 @@ import { TChannelType } from "../api/Channel";
 import { useAppSelector } from "../../hooks";
 import { currFocus } from "../../reducers/focus";
 import { ISentenceData } from "../api/Corpus";
+import "./SentEdit/style.css";
 
 export interface IResNumber {
   translation?: number;
@@ -210,23 +211,13 @@ export const SentEditInner = ({
   );
 
   return (
-    <Card
+    <div
       ref={divRef}
-      bodyStyle={{ paddingBottom: 0, paddingLeft: 0, paddingRight: 0 }}
-      style={{
-        border: isFocus
-          ? "2px solid rgb(0 0 200 / 50%)"
-          : "1px solid rgb(128 128 128 / 10%)",
-        marginTop: 4,
-        borderRadius: 6,
-        backgroundColor: "rgb(255 255 255 / 8%)",
-        width: "100%",
-      }}
-      size="small"
+      className={`sent-edit-inner` + (isFocus ? " sent-focus" : "")}
     >
       {affix ? (
         <Affix offsetTop={44}>
-          <div style={{ backgroundColor: "white" }}>{content}</div>
+          <div className="affix">{content}</div>
         </Affix>
       ) : (
         content
@@ -258,7 +249,7 @@ export const SentEditInner = ({
         onModeChange={(value: ArticleMode | undefined) => setArticleMode(value)}
         onAffix={() => setAffix(!affix)}
       />
-    </Card>
+    </div>
   );
 };
 

+ 3 - 2
dashboard-v4/dashboard/src/components/template/SentEdit/SentAdd.tsx

@@ -5,6 +5,7 @@ import { PlusOutlined } from "@ant-design/icons";
 import { IChannel } from "../../channel/Channel";
 import ChannelTableModal from "../../channel/ChannelTableModal";
 import { TChannelType } from "../../api/Channel";
+import { useIntl } from "react-intl";
 
 interface IWidget {
   disableChannels?: string[];
@@ -17,7 +18,7 @@ const Widget = ({
   onSelect,
 }: IWidget) => {
   const [channelPickerOpen, setChannelPickerOpen] = useState(false);
-
+  const intl = useIntl();
   return (
     <ChannelTableModal
       disableChannels={disableChannels}
@@ -31,7 +32,7 @@ const Widget = ({
             setChannelPickerOpen(true);
           }}
         >
-          Add
+          {intl.formatMessage({ id: "buttons.new" })}
         </Button>
       }
       open={channelPickerOpen}

+ 43 - 42
dashboard-v4/dashboard/src/components/template/SentEdit/SentCanRead.tsx

@@ -105,51 +105,52 @@ const SentCanReadWidget = ({
           onClick={() => load()}
         />
       </div>
-      <SentAdd
-        disableChannels={channels}
-        type={type}
-        onSelect={(channel: IChannel) => {
-          if (typeof user === "undefined") {
-            return;
-          }
-          const newSent: ISentence = {
-            content: "",
-            contentType: "markdown",
-            html: "",
-            book: book,
-            para: para,
-            wordStart: wordStart,
-            wordEnd: wordEnd,
-            editor: {
-              id: user.id,
-              nickName: user.nickName,
-              userName: user.realName,
-            },
-            channel: channel,
-            translationChannels: channelsId,
-            updateAt: "",
-            openInEditMode: true,
-          };
-          setSentData((origin) => {
-            return [newSent, ...origin];
-          });
+      <div style={{ textAlign: "center" }}>
+        <SentAdd
+          disableChannels={channels}
+          type={type}
+          onSelect={(channel: IChannel) => {
+            if (typeof user === "undefined") {
+              return;
+            }
+            const newSent: ISentence = {
+              content: "",
+              contentType: "markdown",
+              html: "",
+              book: book,
+              para: para,
+              wordStart: wordStart,
+              wordEnd: wordEnd,
+              editor: {
+                id: user.id,
+                nickName: user.nickName,
+                userName: user.realName,
+              },
+              channel: channel,
+              translationChannels: channelsId,
+              updateAt: "",
+              openInEditMode: true,
+            };
+            setSentData((origin) => {
+              return [newSent, ...origin];
+            });
 
-          setChannels((origin) => {
-            if (origin) {
-              if (!origin.includes(newSent.channel.id)) {
-                origin.push(newSent.channel.id);
-                return origin;
+            setChannels((origin) => {
+              if (origin) {
+                if (!origin.includes(newSent.channel.id)) {
+                  origin.push(newSent.channel.id);
+                  return origin;
+                }
+              } else {
+                return [newSent.channel.id];
               }
-            } else {
-              return [newSent.channel.id];
+            });
+            if (typeof onCreate !== "undefined") {
+              onCreate();
             }
-          });
-          if (typeof onCreate !== "undefined") {
-            onCreate();
-          }
-        }}
-      />
-
+          }}
+        />
+      </div>
       {sentData.map((item, id) => {
         let diffText: string | null = null;
         if (origin) {

+ 267 - 279
dashboard-v4/dashboard/src/components/template/SentEdit/SentTab.tsx

@@ -101,140 +101,128 @@ const SentTabWidget = ({
     : undefined;
 
   return (
-    <div className="sent_block">
-      <Tabs
-        onMouseEnter={() => setHover(true)}
-        onMouseLeave={() => setHover(false)}
-        activeKey={currKey}
-        type="card"
-        onChange={(activeKey: string) => {
-          setCurrKey(activeKey);
-        }}
-        style={
-          isCompact
-            ? {
-                position: currKey === "close" ? "absolute" : "unset",
-                marginTop: -32,
-                width: "100%",
-                marginRight: 10,
-                backgroundColor:
-                  hover || currKey !== "close"
-                    ? "rgba(128, 128, 128, 0.1)"
-                    : "unset",
+    <Tabs
+      className={
+        "sent_tabs" +
+        (isCompact ? " compact" : "") +
+        (currKey === "close" ? " curr_close" : "")
+      }
+      onMouseEnter={() => setHover(true)}
+      onMouseLeave={() => setHover(false)}
+      activeKey={currKey}
+      type="card"
+      onChange={(activeKey: string) => {
+        setCurrKey(activeKey);
+      }}
+      tabBarStyle={{ marginBottom: 0 }}
+      size="small"
+      tabBarGutter={0}
+      tabBarExtraContent={
+        <Space>
+          <TocPath
+            link="none"
+            data={mPath}
+            channels={channelsId}
+            trigger={path ? path.length > 0 ? path[0].title : <></> : <></>}
+          />
+          <Text>{sentId[0]}</Text>
+          <SentTabCopy wbwData={wbwData} text={`{{${sentId[0]}}}`} />
+          <SentMenu
+            book={book}
+            para={para}
+            loading={magicDictLoading}
+            mode={mode}
+            onMagicDict={(type: string) => {
+              if (typeof onMagicDict !== "undefined") {
+                onMagicDict(type);
               }
-            : {
-                padding: "0 8px",
-                backgroundColor: "rgba(128, 128, 128, 0.1)",
+            }}
+            onMenuClick={(key: string) => {
+              switch (key) {
+                case "compact":
+                  if (typeof onCompact !== "undefined") {
+                    setIsCompact(true);
+                    onCompact(true);
+                  }
+                  break;
+                case "normal":
+                  if (typeof onCompact !== "undefined") {
+                    setIsCompact(false);
+                    onCompact(false);
+                  }
+                  break;
+                case "origin-edit":
+                  if (typeof onModeChange !== "undefined") {
+                    onModeChange("edit");
+                  }
+                  break;
+                case "origin-wbw":
+                  if (typeof onModeChange !== "undefined") {
+                    onModeChange("wbw");
+                  }
+                  break;
+                case "copy-id":
+                  const id = `{{${book}-${para}-${wordStart}-${wordEnd}}}`;
+                  navigator.clipboard.writeText(id).then(() => {
+                    message.success("编号已经拷贝到剪贴板");
+                  });
+                  break;
+                case "copy-link":
+                  let link = `/article/para/${book}-${para}?mode=edit`;
+                  link += `&book=${book}&par=${para}`;
+                  if (channelsId) {
+                    link += `&channel=` + channelsId?.join("_");
+                  }
+                  link += `&focus=${book}-${para}-${wordStart}-${wordEnd}`;
+                  navigator.clipboard.writeText(fullUrl(link)).then(() => {
+                    message.success("链接地址已经拷贝到剪贴板");
+                  });
+                  break;
+                case "affix":
+                  if (typeof onAffix !== "undefined") {
+                    onAffix();
+                  }
+                  break;
+                default:
+                  break;
               }
-        }
-        tabBarStyle={{ marginBottom: 0 }}
-        size="small"
-        tabBarGutter={0}
-        tabBarExtraContent={
-          <Space>
-            <TocPath
-              link="none"
-              data={mPath}
-              channels={channelsId}
-              trigger={path ? path.length > 0 ? path[0].title : <></> : <></>}
-            />
-            <Text>{sentId[0]}</Text>
-            <SentTabCopy wbwData={wbwData} text={`{{${sentId[0]}}}`} />
-            <SentMenu
-              book={book}
-              para={para}
-              loading={magicDictLoading}
-              mode={mode}
-              onMagicDict={(type: string) => {
-                if (typeof onMagicDict !== "undefined") {
-                  onMagicDict(type);
-                }
-              }}
-              onMenuClick={(key: string) => {
-                switch (key) {
-                  case "compact":
-                    if (typeof onCompact !== "undefined") {
-                      setIsCompact(true);
-                      onCompact(true);
-                    }
-                    break;
-                  case "normal":
-                    if (typeof onCompact !== "undefined") {
-                      setIsCompact(false);
-                      onCompact(false);
-                    }
-                    break;
-                  case "origin-edit":
-                    if (typeof onModeChange !== "undefined") {
-                      onModeChange("edit");
-                    }
-                    break;
-                  case "origin-wbw":
-                    if (typeof onModeChange !== "undefined") {
-                      onModeChange("wbw");
-                    }
-                    break;
-                  case "copy-id":
-                    const id = `{{${book}-${para}-${wordStart}-${wordEnd}}}`;
-                    navigator.clipboard.writeText(id).then(() => {
-                      message.success("编号已经拷贝到剪贴板");
-                    });
-                    break;
-                  case "copy-link":
-                    let link = `/article/para/${book}-${para}?mode=edit`;
-                    link += `&book=${book}&par=${para}`;
-                    if (channelsId) {
-                      link += `&channel=` + channelsId?.join("_");
-                    }
-                    link += `&focus=${book}-${para}-${wordStart}-${wordEnd}`;
-                    navigator.clipboard.writeText(fullUrl(link)).then(() => {
-                      message.success("链接地址已经拷贝到剪贴板");
-                    });
-                    break;
-                  case "affix":
-                    if (typeof onAffix !== "undefined") {
-                      onAffix();
-                    }
-                    break;
-                  default:
-                    break;
-                }
-              }}
+            }}
+          />
+        </Space>
+      }
+      items={[
+        {
+          label: (
+            <span style={tabButtonStyle}>
+              <Badge size="small" count={0}>
+                <CloseOutlined />
+              </Badge>
+            </span>
+          ),
+          key: "close",
+          children: <></>,
+        },
+        {
+          label: (
+            <SentTabButton
+              style={tabButtonStyle}
+              icon={<TranslationOutlined />}
+              type="translation"
+              sentId={id}
+              count={
+                currTranNum
+                  ? currTranNum -
+                    (loadedRes?.translation ? loadedRes.translation : 0)
+                  : undefined
+              }
+              title={intl.formatMessage({
+                id: "channel.type.translation.label",
+              })}
             />
-          </Space>
-        }
-        items={[
-          {
-            label: (
-              <span style={tabButtonStyle}>
-                <Badge size="small" count={0}>
-                  <CloseOutlined />
-                </Badge>
-              </span>
-            ),
-            key: "close",
-            children: <></>,
-          },
-          {
-            label: (
-              <SentTabButton
-                style={tabButtonStyle}
-                icon={<TranslationOutlined />}
-                type="translation"
-                sentId={id}
-                count={
-                  currTranNum
-                    ? currTranNum -
-                      (loadedRes?.translation ? loadedRes.translation : 0)
-                    : undefined
-                }
-                title={intl.formatMessage({
-                  id: "channel.type.translation.label",
-                })}
-              />
-            ),
-            key: "translation",
-            children: (
+          ),
+          key: "translation",
+          children: (
+            <div className="content">
               <SentCanRead
                 book={parseInt(sId[0])}
                 para={parseInt(sId[1])}
@@ -244,155 +232,155 @@ const SentTabWidget = ({
                 channelsId={channelsId}
                 onCreate={() => setCurrTranNum((origin) => origin + 1)}
               />
-            ),
-          },
-          {
-            label: (
-              <SentTabButton
-                style={tabButtonStyle}
-                icon={<CloseOutlined />}
-                type="nissaya"
-                sentId={id}
-                count={
-                  currNissayaNum
-                    ? currNissayaNum -
-                      (loadedRes?.nissaya ? loadedRes.nissaya : 0)
-                    : undefined
-                }
-                title={intl.formatMessage({
-                  id: "channel.type.nissaya.label",
-                })}
-              />
-            ),
-            key: "nissaya",
-            children: (
-              <SentCanRead
-                book={parseInt(sId[0])}
-                para={parseInt(sId[1])}
-                wordStart={parseInt(sId[2])}
-                wordEnd={parseInt(sId[3])}
-                type="nissaya"
-                channelsId={channelsId}
-                onCreate={() => setCurrNissayaNum((origin) => origin + 1)}
-              />
-            ),
-          },
-          {
-            label: (
-              <SentTabButton
-                style={tabButtonStyle}
-                icon={<TranslationOutlined />}
-                type="commentary"
-                sentId={id}
-                count={
-                  currCommNum
-                    ? currCommNum -
-                      (loadedRes?.commentary ? loadedRes.commentary : 0)
-                    : undefined
+            </div>
+          ),
+        },
+        {
+          label: (
+            <SentTabButton
+              style={tabButtonStyle}
+              icon={<CloseOutlined />}
+              type="nissaya"
+              sentId={id}
+              count={
+                currNissayaNum
+                  ? currNissayaNum -
+                    (loadedRes?.nissaya ? loadedRes.nissaya : 0)
+                  : undefined
+              }
+              title={intl.formatMessage({
+                id: "channel.type.nissaya.label",
+              })}
+            />
+          ),
+          key: "nissaya",
+          children: (
+            <SentCanRead
+              book={parseInt(sId[0])}
+              para={parseInt(sId[1])}
+              wordStart={parseInt(sId[2])}
+              wordEnd={parseInt(sId[3])}
+              type="nissaya"
+              channelsId={channelsId}
+              onCreate={() => setCurrNissayaNum((origin) => origin + 1)}
+            />
+          ),
+        },
+        {
+          label: (
+            <SentTabButton
+              style={tabButtonStyle}
+              icon={<TranslationOutlined />}
+              type="commentary"
+              sentId={id}
+              count={
+                currCommNum
+                  ? currCommNum -
+                    (loadedRes?.commentary ? loadedRes.commentary : 0)
+                  : undefined
+              }
+              title={intl.formatMessage({
+                id: "channel.type.commentary.label",
+              })}
+            />
+          ),
+          key: "commentary",
+          children: (
+            <SentCanRead
+              book={parseInt(sId[0])}
+              para={parseInt(sId[1])}
+              wordStart={parseInt(sId[2])}
+              wordEnd={parseInt(sId[3])}
+              type="commentary"
+              channelsId={channelsId}
+              onCreate={() => setCurrCommNum((origin) => origin + 1)}
+            />
+          ),
+        },
+        {
+          label: (
+            <SentTabButton
+              icon={<BlockOutlined />}
+              type="original"
+              sentId={id}
+              count={originNum}
+              title={intl.formatMessage({
+                id: "channel.type.original.label",
+              })}
+            />
+          ),
+          key: "original",
+          children: (
+            <SentCanRead
+              book={parseInt(sId[0])}
+              para={parseInt(sId[1])}
+              wordStart={parseInt(sId[2])}
+              wordEnd={parseInt(sId[3])}
+              type="original"
+              origin={origin}
+            />
+          ),
+        },
+        {
+          label: (
+            <SentTabButton
+              style={tabButtonStyle}
+              icon={<BlockOutlined />}
+              type="original"
+              sentId={id}
+              count={currSimilarNum}
+              title={intl.formatMessage({
+                id: "buttons.sim",
+              })}
+            />
+          ),
+          key: "sim",
+          children: (
+            <SentSim
+              book={parseInt(sId[0])}
+              para={parseInt(sId[1])}
+              wordStart={parseInt(sId[2])}
+              wordEnd={parseInt(sId[3])}
+              channelsId={channelsId}
+              limit={5}
+              onCreate={() => setCurrSimilarNum((origin) => origin + 1)}
+            />
+          ),
+        },
+        {
+          label: (
+            <SentTabButtonWbw
+              style={tabButtonStyle}
+              sentId={id}
+              count={0}
+              onMenuClick={(keyPath: string[]) => {
+                switch (keyPath.join("-")) {
+                  case "show-progress":
+                    setShowWbwProgress((origin) => !origin);
+                    break;
                 }
-                title={intl.formatMessage({
-                  id: "channel.type.commentary.label",
-                })}
-              />
-            ),
-            key: "commentary",
-            children: (
-              <SentCanRead
-                book={parseInt(sId[0])}
-                para={parseInt(sId[1])}
-                wordStart={parseInt(sId[2])}
-                wordEnd={parseInt(sId[3])}
-                type="commentary"
-                channelsId={channelsId}
-                onCreate={() => setCurrCommNum((origin) => origin + 1)}
-              />
-            ),
-          },
-          {
-            label: (
-              <SentTabButton
-                icon={<BlockOutlined />}
-                type="original"
-                sentId={id}
-                count={originNum}
-                title={intl.formatMessage({
-                  id: "channel.type.original.label",
-                })}
-              />
-            ),
-            key: "original",
-            children: (
-              <SentCanRead
-                book={parseInt(sId[0])}
-                para={parseInt(sId[1])}
-                wordStart={parseInt(sId[2])}
-                wordEnd={parseInt(sId[3])}
-                type="original"
-                origin={origin}
-              />
-            ),
-          },
-          {
-            label: (
-              <SentTabButton
-                style={tabButtonStyle}
-                icon={<BlockOutlined />}
-                type="original"
-                sentId={id}
-                count={currSimilarNum}
-                title={intl.formatMessage({
-                  id: "buttons.sim",
-                })}
-              />
-            ),
-            key: "sim",
-            children: (
-              <SentSim
-                book={parseInt(sId[0])}
-                para={parseInt(sId[1])}
-                wordStart={parseInt(sId[2])}
-                wordEnd={parseInt(sId[3])}
-                channelsId={channelsId}
-                limit={5}
-                onCreate={() => setCurrSimilarNum((origin) => origin + 1)}
-              />
-            ),
-          },
-          {
-            label: (
-              <SentTabButtonWbw
-                style={tabButtonStyle}
-                sentId={id}
-                count={0}
-                onMenuClick={(keyPath: string[]) => {
-                  switch (keyPath.join("-")) {
-                    case "show-progress":
-                      setShowWbwProgress((origin) => !origin);
-                      break;
-                  }
-                }}
-              />
-            ),
-            key: "wbw",
-            children: (
-              <SentWbw
-                book={parseInt(sId[0])}
-                para={parseInt(sId[1])}
-                wordStart={parseInt(sId[2])}
-                wordEnd={parseInt(sId[3])}
-                channelsId={channelsId}
-                wbwProgress={showWbwProgress}
-              />
-            ),
-          },
-          {
-            label: <span style={tabButtonStyle}>{"关系图"}</span>,
-            key: "relation-graphic",
-            children: <RelaGraphic wbwData={wbwData} />,
-          },
-        ]}
-      />
-    </div>
+              }}
+            />
+          ),
+          key: "wbw",
+          children: (
+            <SentWbw
+              book={parseInt(sId[0])}
+              para={parseInt(sId[1])}
+              wordStart={parseInt(sId[2])}
+              wordEnd={parseInt(sId[3])}
+              channelsId={channelsId}
+              wbwProgress={showWbwProgress}
+            />
+          ),
+        },
+        {
+          label: <span style={tabButtonStyle}>{"关系图"}</span>,
+          key: "relation-graphic",
+          children: <RelaGraphic wbwData={wbwData} />,
+        },
+      ]}
+    />
   );
 };
 

+ 4 - 58
dashboard-v4/dashboard/src/components/template/SentEdit/SentTabButton.tsx

@@ -1,17 +1,4 @@
-import { useIntl } from "react-intl";
-import { Badge, Dropdown } from "antd";
-import type { MenuProps } from "antd";
-import { LinkOutlined, CalendarOutlined } from "@ant-design/icons";
-
-import store from "../../../store";
-import {
-  ISite,
-  refresh as refreshLayout,
-} from "../../../reducers/open-article";
-
-const handleButtonClick = (e: React.MouseEvent<HTMLButtonElement>) => {
-  console.log("click left button", e);
-};
+import { Badge, Space } from "antd";
 
 interface IWidget {
   style?: React.CSSProperties;
@@ -29,52 +16,11 @@ const SentTabButtonWidget = ({
   title,
   count = 0,
 }: IWidget) => {
-  const intl = useIntl();
-  const items: MenuProps["items"] = [
-    {
-      label: "在新标签页中打开",
-      key: "openInWin",
-      icon: <CalendarOutlined />,
-    },
-    {
-      label: intl.formatMessage({
-        id: "buttons.copy.link",
-      }),
-      key: "copyLink",
-      icon: <LinkOutlined />,
-    },
-  ];
-  const handleMenuClick: MenuProps["onClick"] = (e) => {
-    e.domEvent.stopPropagation();
-    switch (e.key) {
-      case "openInCol":
-        const it: ISite = {
-          title: intl.formatMessage({
-            id: `channel.type.${type}.label`,
-          }),
-          url: "corpus_sent/" + type,
-          id: sentId,
-        };
-        store.dispatch(refreshLayout(it));
-        break;
-    }
-  };
-  const menuProps = {
-    items,
-    onClick: handleMenuClick,
-  };
-
   return (
-    <Dropdown.Button
-      style={style}
-      size="small"
-      type="text"
-      menu={menuProps}
-      onClick={handleButtonClick}
-    >
-      {title}
+    <Space>
+      <>{title}</>
       <Badge size="small" color="geekblue" count={count}></Badge>
-    </Dropdown.Button>
+    </Space>
   );
 };
 

+ 74 - 0
dashboard-v4/dashboard/src/components/template/style.css

@@ -21,3 +21,77 @@
 .sent_read_translation:hover ~ .sent_read_interactive_button {
   border: 1px solid black;
 }
+
+.sent-edit-inner {
+  border: 1px solid rgb(129 129 129 / 10%);
+  margin-top: 4px;
+  border-radius: 6px;
+  background-color: rgb(255 255 255 / 8%);
+  width: 100%;
+}
+.sent-focus {
+  border: 2px solid rgb(0 0 200 / 50%);
+}
+
+.sent-edit-inner .affix {
+  background-color: white;
+}
+.sent_tabs {
+  padding: 0 8px;
+  background-color: rgba(128, 128, 128, 0.1);
+}
+
+.sent_tabs.compact {
+  position: unset;
+  margin-top: -32px;
+  width: 100%;
+  margin-right: 10px;
+  background-color: unset;
+}
+
+.sent_tabs:hover .sent_tabs.compact {
+  background-color: rgba(128, 128, 128, 0.1);
+}
+.sent_tabs.compact.curr_close {
+  position: absolute;
+  background-color: rgba(128, 128, 128, 0.1);
+}
+
+.sent_tabs .content {
+  padding: 0 8px;
+}
+
+.sent_tabs .ant-tabs-tab {
+  background: #c6c5c5 !important;
+}
+.sent_tabs .ant-tabs-tab-active {
+  background: rgba(128, 128, 128, 0.1) !important;
+}
+
+/** 2 级 组件 */
+.sent-edit-inner .sent-edit-inner .sent_tabs {
+  background-color: rgba(128, 128, 128, 0.9);
+}
+.sent-edit-inner .sent-edit-inner .sent_tabs .ant-tabs-tab {
+  background: #ececec !important;
+}
+
+.sent-edit-inner .sent-edit-inner .sent_tabs .ant-tabs-tab-active {
+  background: rgba(128, 128, 128, 0.1) !important;
+}
+
+/** 3 级 组件 */
+.sent-edit-inner .sent-edit-inner .sent-edit-inner .sent_tabs {
+  background-color: rgb(200, 200, 200);
+}
+.sent-edit-inner .sent-edit-inner .sent-edit-inner .sent_tabs .ant-tabs-tab {
+  background: #c6c5c5 !important;
+}
+
+.sent-edit-inner
+  .sent-edit-inner
+  .sent-edit-inner
+  .sent_tabs
+  .ant-tabs-tab-active {
+  background: rgba(128, 128, 128, 0.9) !important;
+}

+ 1 - 0
dashboard-v4/dashboard/src/locales/en-US/buttons.ts

@@ -110,6 +110,7 @@ const items = {
   "buttons.clone": "Clone",
   "buttons.general": "General",
   "buttons.ai-models": "AI Models",
+  "buttons.new": "New",
 };
 
 export default items;

+ 1 - 0
dashboard-v4/dashboard/src/locales/zh-Hans/buttons.ts

@@ -111,6 +111,7 @@ const items = {
   "buttons.clone": "克隆",
   "buttons.general": "常规",
   "buttons.ai-models": "AI模型",
+  "buttons.new": "新建",
 };
 
 export default items;

+ 3 - 3
open-ai-server/README.md

@@ -12,11 +12,11 @@ npm install
 
 1. **启动服务器**:
 
-`bash
+```bash
 npm run build
 node dist/main-XXX.js config.json
 
-````
+```
 
 ## 主要功能特点
 
@@ -46,7 +46,7 @@ node dist/main-XXX.js config.json
     "stream": true // 可选,启用流式响应
   }
 }
-````
+```
 
 **非流式响应**:返回完整的 JSON 结果
 **流式响应**:返回 Server-Sent Events 格式的实时数据流