v1.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. <?php
  2. /**
  3. * The docApp widget class file of zin module of ZenTaoPMS.
  4. *
  5. * @copyright Copyright 2024 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net)
  6. * @license ZPL(https://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
  7. * @author Hao Sun <sunhao@easycorp.ltd>
  8. * @package zin
  9. * @link http://www.zentao.net
  10. */
  11. namespace zin;
  12. class docApp extends wg
  13. {
  14. /**
  15. * @var mixed[]
  16. */
  17. protected static $defineProps = array
  18. (
  19. 'width' => '?int|string="100%"', // 宽度。
  20. 'height' => '?int|string="100%"', // 高度。
  21. 'fetcher' => '?string|array', // 数据获取 URL 或回调函数。
  22. 'fetchOnChangeSpace' => '?boolean', // 是否在切换空间时重新获取数据。
  23. 'docFetcher' => '?string|array', // 文档数据获取 URL 或回调函数。
  24. 'libSummariesFetcher' => '?string|array|object', // 落地页库概要数据获取 URL 或回调函数。
  25. 'noSpace' => '?boolean', // 是否不显示空间。
  26. 'noModule' => '?boolean', // 是否不显示模块。
  27. 'data' => '?array', // 数据。
  28. 'spaceType' => '?string', // 空间类型。
  29. 'spaceID' => '?number', // 空间 ID。
  30. 'libID' => '?number', // 库 ID。
  31. 'moduleID' => '?number', // 模块 ID。
  32. 'docID' => '?number', // 文档 ID。
  33. 'docVersion' => '?number', // 文档版本。
  34. 'mode' => '?string', // 应用默认界面模式,包括 home、list、edit、create、files。
  35. 'spaceIcon' => '?string', // 空间图标。
  36. 'libIcon' => '?string', // 库图标。
  37. 'libColors' => '?string[]', // 库颜色列表。
  38. 'libFilesIcon' => '?string', // 库文件图标。
  39. 'moduleIcon' => '?string', // 模块图标。
  40. 'docIcon' => '?string', // 文档图标。
  41. 'fileIcon' => '?array', // 文件图标。
  42. 'fileUrl' => '?string', // 文件下载链接。
  43. 'libTypes' => '?array', // 库类型名称定义。
  44. 'preserve' => '?string', // 是否保留 UI 设置到本地,例如记住侧边栏宽度和状态。
  45. 'userMap' => '?array', // 用户映射定义。
  46. 'currentUser' => '?string', // 当前用户。
  47. 'historyFetcher' => '?string|array|object', // 历史记录数据获取 URL 或回调函数。
  48. 'historyPanel' => '?string|array|object', // 历史记录面板。
  49. 'uploadUrl' => '?string', // 上传文件 URL。
  50. 'downloadUrl' => '?string', // 下载文件 URL。
  51. 'privs' => '?array', // 权限定义。
  52. 'homeName' => '?string|bool', // 首页名称,设置为 false 不显示首页名称。
  53. 'filesFetcher' => '?string|array', // 文件数据获取 URL 或回调函数。
  54. 'search' => '?string', // 搜索关键字。
  55. 'filterType' => '?string', // 过滤类型。
  56. 'pager' => '?PagerInfo', // 分页信息。
  57. 'params' => '?string', // 额外参数信息。
  58. 'orderBy' => '?string', // 排序字段。
  59. 'langData' => '?array', // 语言数据。
  60. 'showLibFiles' => '?boolean|array|string',// 是否显示库文件。
  61. 'getSortableOptions' => '?string', // 获取排序选项。
  62. 'filterItems' => '?string', // 过滤项。
  63. 'isMatchFilter' => '?string', // 是否匹配过滤。
  64. 'getTableOptions' => '?string', // 获取表格选项。
  65. 'getDefaultPager' => '?string', // 获取默认分页。
  66. 'getPagerOptions' => '?string', // 获取分页选项。
  67. 'getSearchBoxOptions' => '?string', // 获取搜索框选项。
  68. 'getActions' => '?string', // 获取操作。
  69. 'getFilterTypes' => '?string', // 获取过滤类型。
  70. 'onCreateDoc' => '?string', // 创建文档事件。
  71. 'onSaveDoc' => '?string', // 保存文档事件。
  72. 'canMoveDoc' => '?string', // 是否可以移动文档。
  73. 'onSwitchView' => '?string', // 切换视图事件。
  74. 'getDocViewSidebarTabs' => '?string', // 获取文档视图侧边栏选项。
  75. 'formatDataItem' => '?string', // 格式化数据条目。
  76. 'viewModeUrl' => '?string', // 应用视图 URL 格式。
  77. 'hasZentaoSlashMenu' => '?boolean' // 是否显示禅道数据。
  78. );
  79. public static function getPageJS()
  80. {
  81. return file_get_contents(__DIR__ . DS . 'js' . DS . 'v1.js');
  82. }
  83. public static function getPageCSS()
  84. {
  85. return file_get_contents(__DIR__ . DS . 'css' . DS . 'v1.css');
  86. }
  87. /**
  88. * 获取禅道数据菜单。
  89. * Get zentao list menu.
  90. *
  91. * @access public
  92. * @return array
  93. */
  94. protected function getZentaoListMenu()
  95. {
  96. global $lang, $config;
  97. $menus = array();
  98. $zentaoList = $config->doc->zentaoList;
  99. $privs = $this->prop('privs');
  100. foreach($zentaoList as $value)
  101. {
  102. $priv = isset($value['priv']) ? $value['priv'] : null;
  103. if($priv && isset($privs[$priv]) && !$privs[$priv]) continue;
  104. if(isset($value['subMenu']))
  105. {
  106. $subMenus = array();
  107. foreach($value['subMenu'] as $subMenu)
  108. {
  109. $priv = isset($subMenu['priv']) ? $subMenu['priv'] : null;
  110. if($priv && isset($privs[$priv]) && !$privs[$priv]) continue;
  111. if($subMenu['key'] == 'ER' && !$config->enableER) continue;
  112. if($subMenu['key'] == 'UR' && !$config->URAndSR) continue;
  113. $subMenus[] = $subMenu;
  114. }
  115. if(empty($subMenus)) continue;
  116. $value['subMenu'] = $subMenus;
  117. }
  118. $menus[] = $value;
  119. }
  120. return $menus;
  121. }
  122. /**
  123. * 转换禅道数据菜单格式
  124. * Convert zentao list menu format.
  125. *
  126. * @access public
  127. * @return array
  128. * @param mixed[] $list
  129. */
  130. protected function convertZentaoListMenu($list)
  131. {
  132. $menus = array();
  133. foreach($list as $item)
  134. {
  135. $menu = array();
  136. if(isset($item['key'])) $menu['key'] = $item['key'];
  137. if(isset($item['name'])) $menu['text'] = $item['name'];
  138. if(isset($item['icon'])) $menu['icon'] = $item['icon'];
  139. if(isset($item['module'])) $menu['module'] = $item['module'];
  140. if(isset($item['method'])) $menu['method'] = $item['method'];
  141. if(isset($item['params'])) $menu['params'] = $item['params'];
  142. if(isset($item['priv'])) $menu['priv'] = $item['priv'];
  143. if(isset($item['isModal'])) $menu['isModal'] = $item['isModal'];
  144. if(isset($item['modalSize'])) $menu['modalSize'] = $item['modalSize'];
  145. if(isset($item['subMenu']))
  146. {
  147. $menu['items'] = $this->convertZentaoListMenu($item['subMenu']);
  148. }
  149. $menus[] = $menu;
  150. }
  151. return $menus;
  152. }
  153. protected function build()
  154. {
  155. global $app, $lang, $config;
  156. jsVar('hocuspocusConnect', $lang->doc->hocuspocusConnect);
  157. jsVar('hocuspocusDisconnect', $lang->doc->hocuspocusDisconnect);
  158. jsVar('enableHocuspocus', $config->docHocuspocus->enable);
  159. jsVar('needReadable', $lang->doc->needReadable);
  160. jsVar('vision', $config->vision);
  161. jsVar('isInModal', isInModal());
  162. jsVar('userGroups', $app->user->groups);
  163. jsVar('canViewAITask', !empty($config->enableAITeammate) && $config->edition != 'open' && hasPriv('aitask', 'browse'));
  164. /**
  165. * 定义文档应用接口链接。
  166. * Define the fetcher links for doc app.
  167. */
  168. $fetcher = createLink('doc', 'ajaxGetSpaceData', 'type={spaceType}&spaceID={spaceID}&picks={picks}');
  169. $docFetcher = createLink('doc', 'ajaxGetDoc', 'docID={docID}&version={version}');
  170. $filesFetcher = createLink('doc', 'ajaxGetFiles', 'type={objectType}&objectID={objectID}');
  171. $libSummariesFetcher = createLink('doc', 'ajaxGetLibSummaries', 'spaceType={spaceType}&spaceList={spaceList}');
  172. $uploadUrl = createLink('file', 'ajaxUpload', 'uid={uid}&objectType={objectType}&objectID={objectID}&extra={extra}&field={field}&api={api}&gid={gid}');
  173. $downloadUrl = createLink('file', 'ajaxQuery', 'fileID={gid}&objectType={objectType}&objectID={objectID}&title={title}&extra={extra}');
  174. $fileInfoUrl = createLink('file', 'ajaxQuery', 'fileID={gid}&objectType={objectType}&objectID={objectID}&title={title}&extra={extra}', 'json');
  175. /**
  176. * 定义文档界面上的文件下载链接。
  177. * Define the file download link for doc app.
  178. */
  179. $sessionStr = session_name() . '=' . session_id();
  180. $fileUrl = $this->prop('fileUrl');
  181. if(empty($fileUrl))
  182. {
  183. $fileUrl = createLink('file', 'download', 'fileID={id}');
  184. $fileUrl .= strpos($fileUrl, '?') === false ? '?' : '&';
  185. $fileUrl .= $sessionStr;
  186. }
  187. /**
  188. * Setting language data for frontend. Use getLang('xxx') in js/app.ui.js.
  189. * 设置前端语言数据。 在 js/app.ui.js 中使用 getLang('xxx') 来访问语言数据。
  190. */
  191. $app->loadLang('file');
  192. $app->loadLang('api');
  193. $langData = $lang->doc->docLang;
  194. $langData->filePreview = $lang->file->preview;
  195. $langData->fileDownload = $lang->file->download;
  196. $langData->fileDelete = $lang->file->delete;
  197. $langData->fileRename = $lang->file->edit;
  198. $langData->fileConfirmDelete = $lang->file->confirmDelete;
  199. $langData->deleted = $lang->file->deleted;
  200. $langData->createApi = $lang->api->createApi;
  201. $langData->apifilterTypes = $lang->api->filterTypes;
  202. $langData->module = $lang->api->module;
  203. $langData->struct = $lang->api->struct;
  204. $langData->releases = $lang->api->releases;
  205. $langData->noApi = $lang->api->noApi;
  206. $langData->version = $lang->api->version;
  207. $langData->defaultVersion = $lang->api->defaultVersion;
  208. $langData->createStruct = $lang->api->createStruct;
  209. $langData->createRelease = $lang->api->createRelease;
  210. $langData->libTypeList = $lang->api->libTypeList;
  211. $langData->latestVersion = $lang->api->latestVersion;
  212. $langData->template = $lang->doc->template;
  213. $langData->labelAllVersions = $lang->doc->allVersion;
  214. $langData->labelDiff = $lang->doc->diff;
  215. $langData->labelConfirm = $lang->doc->confirm;
  216. $langData->labelCancelDiff = $lang->doc->cancelDiff;
  217. /**
  218. * 通过语言项定义文档表格列显示名称。
  219. * Define the table columns for doc app.
  220. */
  221. $langData->tableCols = array();
  222. $langData->tableCols['id'] = $lang->doc->id;
  223. $langData->tableCols['title'] = $lang->doc->title;
  224. $langData->tableCols['collects'] = $lang->doc->collect;
  225. $langData->tableCols['views'] = $lang->doc->views;
  226. $langData->tableCols['addedBy'] = $lang->doc->addedBy;
  227. $langData->tableCols['addedDate'] = $lang->doc->addedDate;
  228. $langData->tableCols['editedBy'] = $lang->doc->editedBy;
  229. $langData->tableCols['editedDate'] = $lang->doc->editedDate;
  230. $langData->tableCols['actions'] = $lang->actions;
  231. /**
  232. * 通过语言项定义附件表格列显示名称。
  233. * Define the files table columns for doc app.
  234. */
  235. $langData->fileTableCols = array();
  236. $langData->fileTableCols['id'] = $lang->idAB;
  237. $langData->fileTableCols['title'] = $lang->doc->fileTitle;
  238. $langData->fileTableCols['objectName'] = $lang->doc->source;
  239. $langData->fileTableCols['extension'] = $lang->doc->extension;
  240. $langData->fileTableCols['size'] = $lang->doc->size;
  241. $langData->fileTableCols['addedBy'] = $lang->doc->addedBy;
  242. $langData->fileTableCols['addedDate'] = $lang->doc->addedDate;
  243. $langData->fileTableCols['actions'] = $lang->actions;
  244. /**
  245. * 合并语言数据。
  246. * Merge the language data.
  247. */
  248. if($this->hasProp('langData')) $langData = array_merge((array)$langData, (array)$this->prop('langData'));
  249. /**
  250. * 界面模式 URL 格式化模版。
  251. * URL format for view mode change.
  252. */
  253. $viewModeUrl = $this->prop('viewModeUrl');
  254. $spaceType = $this->hasProp('spaceType') ? $this->prop('spaceType') : data('spaceType');
  255. $rawModule = $app->rawModule;
  256. $rawMethod = $app->rawMethod;
  257. if(!$this->hasProp('viewModeUrl'))
  258. {
  259. if ($rawModule == 'doc' && $rawMethod == 'app')
  260. {
  261. $viewModeUrl = createLink('doc', 'app', 'type={spaceType}&spaceID={spaceID}&libID={libID}&moduleID={moduleID}&docID={docID}&mode={mode}&orderBy={orderBy}&recTotal={recTotal}&recPerPage={recPerPage}&pageID={page}&filterType={filterType}&search={search}&noSpace={noSpace}');
  262. }
  263. else
  264. {
  265. if($rawModule == 'doc' && $rawMethod == 'view')
  266. {
  267. $spaceMethod = array('mine' => 'mySpace', 'custom' => 'teamSpace', 'project' => 'projectSpace', 'product' => 'productSpace');
  268. if(isset($spaceMethod[$spaceType])) $rawMethod = $spaceMethod[$spaceType];
  269. }
  270. $viewModeUrl = createLink($rawModule, $rawMethod, 'objectID={spaceID}&libID={libID}&moduleID={moduleID}&browseType={filterType}&orderBy={orderBy}&param=0&recTotal={recTotal}&recPerPage={recPerPage}&pageID={page}&mode={mode}&docID={docID}&search={search}');
  271. }
  272. }
  273. $hasZentaoSlashMenu = $this->prop('hasZentaoSlashMenu');
  274. if($hasZentaoSlashMenu === null ) $hasZentaoSlashMenu = true;
  275. $app->control->loadModel('file');
  276. $fileListProps = array();
  277. $canDownloadFile = common::hasPriv('file', 'download');
  278. if($canDownloadFile)
  279. {
  280. $previewLink = helper::createLink('file', 'download', "fileID={id}&mouse=left");
  281. jsVar('previewLang', $lang->file->preview);
  282. jsVar('downloadLang', $lang->file->download);
  283. jsVar('previewLink', $previewLink);
  284. jsVar('downloadLink', $fileUrl);
  285. jsVar('libreOfficeTurnon', isset($config->file->libreOfficeTurnon) && $config->file->libreOfficeTurnon == 1);
  286. $fileListProps['fileUrl'] = $fileUrl;
  287. $fileListProps['target'] = '_blank';
  288. $fileListProps['hoverItemActions'] = true;
  289. $fileListProps['itemProps'] = array('target' => '_blank');
  290. $fileListProps['fileActions'] = jsCallback('file')->do('return getFileActions(file)');
  291. }
  292. else
  293. {
  294. $fileUrl = '';
  295. }
  296. $canPreviewOffice = common::hasPriv('file', 'preview') && isset($config->file->libreOfficeTurnon) and $config->file->libreOfficeTurnon == 1;
  297. $historyPanelProps = $this->prop('historyPanel');
  298. if(empty($historyPanelProps)) $historyPanelProps = array();
  299. if(is_array($historyPanelProps)) $historyPanelProps['fileListProps'] = $fileListProps;
  300. /* 对比不可用场景:文档模板、API 文档、开源版 */
  301. $diffEnabled = ($config->edition != 'open')
  302. && !($rawModule == 'doc' && $rawMethod == 'view')
  303. && $rawModule != 'api';
  304. /* 禅道数据菜单不可用界面:运营(lite)、需求与市场(or) */
  305. if ($config->vision == 'lite' || $config->vision == 'or') $hasZentaoSlashMenu = false;
  306. $zentaoListMenu = $hasZentaoSlashMenu ? $this->getZentaoListMenu() : array();
  307. $moreMenus = $zentaoListMenu
  308. ? array_merge([array(
  309. 'text' => $lang->doc->zentaoData,
  310. 'type' => 'heading',
  311. )], $this->convertZentaoListMenu($zentaoListMenu))
  312. : array();
  313. $privs = $this->prop('privs', []);
  314. if(!isset($privs['downloadFile'])) $privs['downloadFile'] = $canDownloadFile;
  315. if(!isset($privs['previewFile'])) $privs['previewFile'] = $canPreviewOffice;
  316. return zui::docApp
  317. (
  318. set::_class('shadow rounded ring canvas'),
  319. set::_style(array('height' => 'calc(100vh - 72px)')),
  320. set::_id('docApp'),
  321. set::token(session_id()),
  322. set::awarenessUser(array('id' => $app->user->id, 'account' => $app->user->account, 'name' => $app->user->realname, 'avatar' => $app->user->avatar)),
  323. set::useHocuspocus(jsRaw('window.useHocuspocus')),
  324. set::hocuspocusUrls(array($config->docHocuspocus->http, $config->docHocuspocus->websocket)),
  325. set::onHocuspocusConnect(jsRaw('window.onHocuspocusConnect')),
  326. set::onHocuspocusDisconnect(jsRaw('window.onHocuspocusDisconnect')),
  327. set::onAuthenticationFailed(jsRaw('window.onAuthenticationFailed')),
  328. set::onModeChange(jsRaw('window.onDocAppModeChange')),
  329. set::spaceType($spaceType),
  330. set::spaceID(data('spaceID')),
  331. set::libID(data('libID')),
  332. set::moduleID(data('moduleID')),
  333. set::docID(data('docID')),
  334. set::docVersion(data('docVersion')),
  335. set::diffEnabled($diffEnabled),
  336. set::mode('list'),
  337. set::filterType(data('filterType')),
  338. set::search(data('search')),
  339. set::orderBy(data('orderBy')),
  340. set::pager(array('recTotal' => 0, 'recPerPage' => 20, 'page' => 1)),
  341. set::fetcher($fetcher),
  342. set::docFetcher($docFetcher),
  343. set::filesFetcher($filesFetcher),
  344. set::libSummariesFetcher($libSummariesFetcher),
  345. set::width('100%'),
  346. set::height('100%'),
  347. set::userMap(data('users')),
  348. set::currentUser($app->user->account),
  349. set::uploadUrl($uploadUrl),
  350. set::downloadUrl($downloadUrl),
  351. set::sessionStr($sessionStr),
  352. set('$options', jsRaw('window.setDocAppOptions')),
  353. set($this->props),
  354. set::privs($privs),
  355. set::fileUrl($fileUrl),
  356. set::viewModeUrl($viewModeUrl),
  357. set::langData($langData),
  358. set::historyPanel($historyPanelProps),
  359. set::showToolbar(true),
  360. set::moreMenu($moreMenus),
  361. set::moreMenuAction(jsRaw('window.moreMenuAction')),
  362. set::canPreviewOffice($canPreviewOffice),
  363. set::fileInfoUrl($fileInfoUrl),
  364. $hasZentaoSlashMenu ? jsCall('setZentaoSlashMenu', $zentaoListMenu, $lang->doc->zentaoData, $config->doc->zentaoListMenuPosition) : null
  365. );
  366. }
  367. }