feat: integrate ZYT admin with IAM Hub
This commit is contained in:
+19
-1
@@ -28,7 +28,25 @@ export function getWorkWechatConfig() {
|
||||
|
||||
// 企业微信授权登录
|
||||
export function workWechatLogin(params: { code: string }) {
|
||||
return request.post({ url: '/login/workWechatLogin', params: { ...params, terminal: config.terminal } })
|
||||
return request.post({
|
||||
url: '/login/workWechatLogin',
|
||||
params: { ...params, terminal: config.terminal }
|
||||
})
|
||||
}
|
||||
|
||||
// IAM Hub 登录配置
|
||||
export function getIamHubConfig() {
|
||||
return request.get({ url: '/login/iamConfig' })
|
||||
}
|
||||
|
||||
// IAM Hub 一次性回调码换取本平台会话
|
||||
export function iamHubExchange(params: { code: string }) {
|
||||
return request.post({ url: '/login/iamExchange', params })
|
||||
}
|
||||
|
||||
export function getIamHubStartUrl() {
|
||||
const baseUrl = config.baseUrl.replace(/\/+$/, '')
|
||||
return `${baseUrl}/${config.urlPrefix}/login/iamStart`
|
||||
}
|
||||
|
||||
// 绑定企业微信
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
import { getUserInfo, login, logout, workWechatLogin } from '@/api/user'
|
||||
import { getUserInfo, iamHubExchange, login, logout, workWechatLogin } from '@/api/user'
|
||||
import { TOKEN_KEY } from '@/enums/cacheEnums'
|
||||
import { PageEnum } from '@/enums/pageEnum'
|
||||
import router, { filterAsyncRoutes } from '@/router'
|
||||
@@ -31,6 +31,12 @@ const useUserStore = defineStore({
|
||||
}),
|
||||
getters: {},
|
||||
actions: {
|
||||
applyLoginResult(data: any) {
|
||||
this.token = data.token
|
||||
this.isPaw = data.is_paw ?? 1
|
||||
cache.set(TOKEN_KEY, data.token)
|
||||
return data
|
||||
},
|
||||
resetState() {
|
||||
this.token = ''
|
||||
this.userInfo = {}
|
||||
@@ -45,10 +51,7 @@ const useUserStore = defineStore({
|
||||
password: password
|
||||
})
|
||||
.then((data) => {
|
||||
this.token = data.token
|
||||
this.isPaw = data.is_paw ?? 1
|
||||
cache.set(TOKEN_KEY, data.token)
|
||||
resolve(data)
|
||||
resolve(this.applyLoginResult(data))
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error)
|
||||
@@ -73,16 +76,20 @@ const useUserStore = defineStore({
|
||||
return new Promise((resolve, reject) => {
|
||||
workWechatLogin({ code })
|
||||
.then((data) => {
|
||||
this.token = data.token
|
||||
this.isPaw = data.is_paw ?? 1
|
||||
cache.set(TOKEN_KEY, data.token)
|
||||
resolve(data)
|
||||
resolve(this.applyLoginResult(data))
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
iamHubLogin(code: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
iamHubExchange({ code })
|
||||
.then((data) => resolve(this.applyLoginResult(data)))
|
||||
.catch(reject)
|
||||
})
|
||||
},
|
||||
getUserInfo() {
|
||||
return new Promise((resolve, reject) => {
|
||||
getUserInfo()
|
||||
|
||||
@@ -11,11 +11,13 @@
|
||||
<div class="text-center text-3xl font-medium mb-8">{{ config.web_name }}</div>
|
||||
|
||||
<!-- 企业微信自动授权中 -->
|
||||
<div v-if="wxWorkAutoLogin" class="text-center py-10">
|
||||
<div v-if="wxWorkAutoLogin || iamAutoLogin" class="text-center py-10">
|
||||
<el-icon class="is-loading mb-4" :size="40" color="var(--el-color-primary)">
|
||||
<Loading />
|
||||
</el-icon>
|
||||
<div class="text-gray-500">企业微信授权登录中...</div>
|
||||
<div class="text-gray-500">
|
||||
{{ iamAutoLogin ? '公司统一身份登录中...' : '企业微信授权登录中...' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
@@ -58,21 +60,42 @@
|
||||
<div class="mb-5">
|
||||
<el-checkbox v-model="remAccount" label="记住账号"></el-checkbox>
|
||||
</div>
|
||||
<el-button type="primary" size="large" :loading="isLock" @click="lockLogin">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="large"
|
||||
:loading="isLock"
|
||||
@click="lockLogin"
|
||||
>
|
||||
登录
|
||||
</el-button>
|
||||
<template v-if="iamHubEnabled">
|
||||
<el-divider>或</el-divider>
|
||||
<el-button size="large" class="w-full" @click="handleIamHubLogin">
|
||||
公司员工统一登录
|
||||
</el-button>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- 企业微信扫码登录(非企业微信内浏览器) -->
|
||||
<template v-if="loginMode === 'wxwork'">
|
||||
<div class="wxwork-qrcode-wrap">
|
||||
<div v-if="wxWorkLoading" class="text-center py-10">
|
||||
<el-icon class="is-loading" :size="32" color="var(--el-color-primary)">
|
||||
<el-icon
|
||||
class="is-loading"
|
||||
:size="32"
|
||||
color="var(--el-color-primary)"
|
||||
>
|
||||
<Loading />
|
||||
</el-icon>
|
||||
<div class="mt-2 text-gray-400 text-sm">加载企业微信扫码...</div>
|
||||
<div class="mt-2 text-gray-400 text-sm">
|
||||
加载企业微信扫码...
|
||||
</div>
|
||||
</div>
|
||||
<div v-else id="wxwork_qrcode_container" class="wxwork-qrcode"></div>
|
||||
<div
|
||||
v-else
|
||||
id="wxwork_qrcode_container"
|
||||
class="wxwork-qrcode"
|
||||
></div>
|
||||
</div>
|
||||
<div class="text-center text-sm text-gray-400 mt-4">
|
||||
请使用企业微信扫描二维码登录
|
||||
@@ -99,7 +122,7 @@ import LayoutFooter from '@/layout/components/footer.vue'
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import cache from '@/utils/cache'
|
||||
import { getWorkWechatConfig } from '@/api/user'
|
||||
import { getIamHubConfig, getIamHubStartUrl, getWorkWechatConfig } from '@/api/user'
|
||||
|
||||
const passwordRef = shallowRef<InputInstance>()
|
||||
const formRef = shallowRef<FormInstance>()
|
||||
@@ -124,6 +147,8 @@ const wxWorkEnabled = ref(false)
|
||||
const wxWorkConfig = ref<{ corp_id: string; agent_id: string }>({ corp_id: '', agent_id: '' })
|
||||
const wxWorkAutoLogin = ref(false)
|
||||
const wxWorkLoading = ref(false)
|
||||
const iamHubEnabled = ref(false)
|
||||
const iamAutoLogin = ref(false)
|
||||
|
||||
const isInWxWork = () => /wxwork/i.test(navigator.userAgent)
|
||||
|
||||
@@ -160,8 +185,14 @@ const handleLogin = async () => {
|
||||
|
||||
const { isLock, lockFn: lockLogin } = useLockFn(handleLogin)
|
||||
|
||||
const handleIamHubLogin = () => {
|
||||
window.location.href = getIamHubStartUrl()
|
||||
}
|
||||
|
||||
const redirectAfterLogin = () => {
|
||||
const { query: { redirect } } = route
|
||||
const {
|
||||
query: { redirect }
|
||||
} = route
|
||||
const path = typeof redirect === 'string' ? redirect : PageEnum.INDEX
|
||||
router.push(path)
|
||||
}
|
||||
@@ -190,6 +221,22 @@ const handleWxWorkCodeLogin = async (code: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleIamHubCodeLogin = async (code: string) => {
|
||||
iamAutoLogin.value = true
|
||||
try {
|
||||
const result: any = await userStore.iamHubLogin(code)
|
||||
if (result.need_bind_work_wechat) {
|
||||
router.push('/bind-work-wechat')
|
||||
return
|
||||
}
|
||||
redirectAfterLogin()
|
||||
} catch (error: any) {
|
||||
console.error('IAM Hub 登录失败:', error)
|
||||
ElMessage.error(error?.msg || error?.message || '公司统一身份登录失败,请重新发起登录')
|
||||
iamAutoLogin.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载企业微信扫码 JS SDK 并渲染二维码
|
||||
const renderWxWorkQRCode = async () => {
|
||||
if (!wxWorkConfig.value.corp_id) return
|
||||
@@ -222,7 +269,7 @@ const renderWxWorkQRCode = async () => {
|
||||
lang: 'zh',
|
||||
state: 'admin_login',
|
||||
href: '',
|
||||
self_redirect: false,
|
||||
self_redirect: false
|
||||
})
|
||||
}
|
||||
|
||||
@@ -241,6 +288,27 @@ onMounted(async () => {
|
||||
formData.account = value.account
|
||||
}
|
||||
|
||||
const hashParams = new URLSearchParams(window.location.hash.replace(/^#/, ''))
|
||||
const iamCode = hashParams.get('iam_code')
|
||||
const iamError = hashParams.get('iam_error')
|
||||
if (iamCode || iamError) {
|
||||
window.history.replaceState({}, '', window.location.pathname + window.location.search)
|
||||
}
|
||||
if (iamCode) {
|
||||
handleIamHubCodeLogin(iamCode)
|
||||
return
|
||||
}
|
||||
if (iamError) {
|
||||
ElMessage.error('公司统一身份登录失败,请重新发起登录')
|
||||
}
|
||||
|
||||
try {
|
||||
const iamConfig = await getIamHubConfig()
|
||||
iamHubEnabled.value = iamConfig?.enabled === true
|
||||
} catch (e) {
|
||||
console.warn('获取 IAM Hub 登录配置失败:', e)
|
||||
}
|
||||
|
||||
// 检查 URL 中是否有企业微信回调 code
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const wxCode = urlParams.get('code')
|
||||
@@ -248,7 +316,9 @@ onMounted(async () => {
|
||||
|
||||
if (wxCode && wxState === 'admin_login') {
|
||||
// 清除 URL 中的 code 参数(避免刷新重复使用)
|
||||
const cleanUrl = window.location.pathname + (route.query.redirect ? `?redirect=${route.query.redirect}` : '')
|
||||
const cleanUrl =
|
||||
window.location.pathname +
|
||||
(route.query.redirect ? `?redirect=${route.query.redirect}` : '')
|
||||
window.history.replaceState({}, '', cleanUrl)
|
||||
|
||||
handleWxWorkCodeLogin(wxCode)
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# IAM Hub 员工统一登录接入
|
||||
|
||||
甄养堂管理后台使用 IAM Hub 作为员工身份、登录会话和统一停权来源;甄养堂原有角色、菜单、部门、岗位和业务数据权限继续保留在本项目中。
|
||||
|
||||
## 接入流程
|
||||
|
||||
1. 登录页调用 `/adminapi/login/iamConfig` 判断统一登录是否启用。
|
||||
2. 员工点击“公司员工统一登录”,浏览器进入 `/adminapi/login/iamStart`。
|
||||
3. 服务端生成一次性 `state`、`nonce` 和 PKCE verifier,并跳转 Keycloak。
|
||||
4. `/adminapi/login/iamCallback` 交换授权码,校验 JWT 签名、Issuer、Audience、有效期、nonce 和 `at_hash`。
|
||||
5. 服务端使用应用凭证查询 IAM Hub 开户上下文;已有管理员按绑定ID复用,新员工延迟创建本地管理员。
|
||||
6. 本地管理员ID以服务端验证方式回写 IAM Hub,历史角色、部门、岗位和业务数据不迁移。
|
||||
7. 本地登录令牌通过60秒一次性交换码放在 URL fragment 中,前端消费后立即清理地址栏。
|
||||
8. IAM Hub 调用 `/adminapi/iam/revocation`,甄养堂验证原始请求体HMAC,禁用管理员、撤销全部会话并记录幂等审计。
|
||||
|
||||
## 数据库和配置
|
||||
|
||||
先执行:
|
||||
|
||||
```text
|
||||
server/sql/1.9.20260908/add_iam_hub_admin_identity.sql
|
||||
```
|
||||
|
||||
再参考 `server/.env.iam-hub.example` 配置应用凭证、OIDC客户端和停权密钥。`DEFAULT_ROLE_IDS` 只用于首次创建的新管理员;已有账号绑定后继续使用原角色。
|
||||
|
||||
生产回调地址和 Keycloak 客户端必须一致。应用凭证与OIDC密钥只写部署环境,不提交仓库。
|
||||
Executable
+28
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT=$(cd "$(dirname "$0")/.." && pwd)
|
||||
cd "$ROOT"
|
||||
|
||||
php server/tests/IamHubIntegrationTest.php >/dev/null
|
||||
echo ZYT_IAM_UNIT_CONTRACT=PASS
|
||||
php server/tests/AdminMultiRoleRegressionTest.php >/dev/null
|
||||
php server/tests/WecomPromotionPoolOperatorContractTest.php >/dev/null
|
||||
echo ZYT_EXISTING_AUTH_REGRESSION=PASS
|
||||
|
||||
find server/app/common/service/iam server/app/common/model/iam -type f -name '*.php' -print0 \
|
||||
| xargs -0 -n1 php -l >/dev/null
|
||||
php -l server/app/adminapi/controller/IamController.php >/dev/null
|
||||
php -l server/app/adminapi/controller/LoginController.php >/dev/null
|
||||
php -l server/app/adminapi/validate/LoginValidate.php >/dev/null
|
||||
echo ZYT_IAM_PHP_LINT=PASS
|
||||
|
||||
grep -Rqs '公司员工统一登录' server/public/admin/assets
|
||||
grep -Rqs '/login/iamExchange' server/public/admin/assets
|
||||
grep -Rqs 'iam_code' server/public/admin/assets
|
||||
echo ZYT_IAM_FRONTEND_DIST=PASS
|
||||
|
||||
grep -q 'uk_admin_iam_subject' server/sql/1.9.20260908/add_iam_hub_admin_identity.sql
|
||||
grep -q 'uk_iam_revocation_event_id' server/sql/1.9.20260908/add_iam_hub_admin_identity.sql
|
||||
echo ZYT_IAM_MIGRATION_CONTRACT=PASS
|
||||
echo ZYT_IAM_ADAPTER_VERIFIED
|
||||
@@ -0,0 +1,16 @@
|
||||
[IAM_HUB]
|
||||
ENABLED = true
|
||||
BASE_URL = "http://iam-hub-api:8080"
|
||||
APPLICATION_ID = "zyt"
|
||||
APPLICATION_TOKEN = ""
|
||||
REVOCATION_SECRET = ""
|
||||
OIDC_ISSUER = "https://iam.example.com/realms/iam-hub"
|
||||
OIDC_INTERNAL_BASE_URL = "http://keycloak:8080/realms/iam-hub"
|
||||
OIDC_CLIENT_ID = "zyt"
|
||||
OIDC_CLIENT_SECRET = ""
|
||||
OIDC_REDIRECT_URI = "https://admin.example.com/adminapi/login/iamCallback"
|
||||
LOGIN_SUCCESS_URL = "https://admin.example.com/admin/login"
|
||||
DEFAULT_ROLE_IDS = ""
|
||||
STATE_TTL_SECONDS = 300
|
||||
EXCHANGE_TTL_SECONDS = 60
|
||||
HTTP_TIMEOUT_SECONDS = 10
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\adminapi\controller;
|
||||
|
||||
use app\common\service\iam\IamHubException;
|
||||
use app\common\service\iam\IamRevocationService;
|
||||
use think\facade\Log;
|
||||
|
||||
class IamController extends BaseAdminController
|
||||
{
|
||||
public array $notNeedLogin = ['revocation'];
|
||||
|
||||
public function revocation()
|
||||
{
|
||||
try {
|
||||
(new IamRevocationService())->apply(
|
||||
(string) $this->request->getContent(),
|
||||
(string) $this->request->header('X-IAM-Signature', ''),
|
||||
(string) $this->request->header('X-IAM-Event-ID', '')
|
||||
);
|
||||
return response('', 204);
|
||||
} catch (IamHubException $error) {
|
||||
return json([
|
||||
'code' => 0,
|
||||
'show' => 0,
|
||||
'msg' => $error->getMessage(),
|
||||
'data' => [],
|
||||
], $error->httpStatus());
|
||||
} catch (\Throwable $error) {
|
||||
Log::error('IAM Hub revocation failed: ' . get_class($error));
|
||||
return json([
|
||||
'code' => 0,
|
||||
'show' => 0,
|
||||
'msg' => 'IAM Hub revocation failed',
|
||||
'data' => [],
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,11 @@ namespace app\adminapi\controller;
|
||||
|
||||
use app\adminapi\logic\LoginLogic;
|
||||
use app\adminapi\validate\LoginValidate;
|
||||
use app\common\service\iam\IamAdminIdentityService;
|
||||
use app\common\service\iam\IamExchangeService;
|
||||
use app\common\service\iam\IamHubException;
|
||||
use app\common\service\iam\IamOidcService;
|
||||
use think\facade\Log;
|
||||
|
||||
/**
|
||||
* 管理员登录控制器
|
||||
@@ -24,7 +29,57 @@ use app\adminapi\validate\LoginValidate;
|
||||
*/
|
||||
class LoginController extends BaseAdminController
|
||||
{
|
||||
public array $notNeedLogin = ['account', 'workWechatConfig', 'workWechatLogin', 'checkDbColumn', 'changeFirstPassword'];
|
||||
public array $notNeedLogin = [
|
||||
'account', 'workWechatConfig', 'workWechatLogin', 'checkDbColumn', 'changeFirstPassword',
|
||||
'iamConfig', 'iamStart', 'iamCallback', 'iamExchange',
|
||||
];
|
||||
|
||||
public function iamConfig()
|
||||
{
|
||||
return $this->data([
|
||||
'enabled' => (new IamOidcService())->enabled(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function iamStart()
|
||||
{
|
||||
try {
|
||||
return redirect((new IamOidcService())->begin());
|
||||
} catch (IamHubException $error) {
|
||||
return json(['code' => 0, 'show' => 1, 'msg' => $error->getMessage(), 'data' => []], $error->httpStatus());
|
||||
}
|
||||
}
|
||||
|
||||
public function iamCallback()
|
||||
{
|
||||
$successUrl = (string) config('iam_hub.login_success_url', '/admin/login');
|
||||
try {
|
||||
$code = trim((string) $this->request->get('code', ''));
|
||||
$state = trim((string) $this->request->get('state', ''));
|
||||
if ($code === '' || $state === '') {
|
||||
throw new IamHubException('统一身份登录回调参数缺失');
|
||||
}
|
||||
$claims = (new IamOidcService())->callback($code, $state);
|
||||
$login = (new IamAdminIdentityService())->login($claims, 1);
|
||||
$exchange = new IamExchangeService((int) config('iam_hub.exchange_ttl_seconds', 60));
|
||||
$exchangeCode = $exchange->store($login);
|
||||
return redirect($successUrl . '#iam_code=' . rawurlencode($exchangeCode));
|
||||
} catch (\Throwable $error) {
|
||||
Log::error('IAM Hub admin login callback failed: ' . get_class($error));
|
||||
return redirect($successUrl . '#iam_error=1');
|
||||
}
|
||||
}
|
||||
|
||||
public function iamExchange()
|
||||
{
|
||||
$code = trim((string) $this->request->post('code', ''));
|
||||
$exchange = new IamExchangeService((int) config('iam_hub.exchange_ttl_seconds', 60));
|
||||
$login = $exchange->consume($code);
|
||||
if ($login === null) {
|
||||
return $this->fail('统一身份登录凭证无效或已过期');
|
||||
}
|
||||
return $this->data($login);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 账号登录
|
||||
@@ -161,4 +216,4 @@ class LoginController extends BaseAdminController
|
||||
(new LoginLogic())->logout($this->adminInfo);
|
||||
return $this->success();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ class LoginValidate extends BaseValidate
|
||||
// 任一不满足都走原始密码校验。每次免密登录都会记一条 warning 日志,便于发现误开。
|
||||
if (self::isLocalDebugLoginBypassEnabled()) {
|
||||
$adminInfo = Admin::where('account', '=', $data['account'])
|
||||
->field(['password,disable'])
|
||||
->field(['password,disable,iam_managed'])
|
||||
->findOrEmpty();
|
||||
if ($adminInfo->isEmpty()) {
|
||||
return '账号不存在';
|
||||
@@ -68,6 +68,9 @@ class LoginValidate extends BaseValidate
|
||||
if ((int)($adminInfo['disable'] ?? 0) === 1) {
|
||||
return '账号已禁用';
|
||||
}
|
||||
if ((int) ($adminInfo['iam_managed'] ?? 0) === 1) {
|
||||
return '请使用公司员工统一登录';
|
||||
}
|
||||
Log::warning(sprintf(
|
||||
'[LOCAL_DEBUG] login bypass password: account=%s, ip=%s',
|
||||
(string)($data['account'] ?? ''),
|
||||
@@ -96,7 +99,7 @@ class LoginValidate extends BaseValidate
|
||||
}
|
||||
|
||||
$adminInfo = Admin::where('account', '=', $data['account'])
|
||||
->field(['password,disable'])
|
||||
->field(['password,disable,iam_managed'])
|
||||
->findOrEmpty();
|
||||
|
||||
if ($adminInfo->isEmpty()) {
|
||||
@@ -107,6 +110,10 @@ class LoginValidate extends BaseValidate
|
||||
return '账号已禁用';
|
||||
}
|
||||
|
||||
if ((int) ($adminInfo['iam_managed'] ?? 0) === 1) {
|
||||
return '请使用公司员工统一登录';
|
||||
}
|
||||
|
||||
if (empty($adminInfo['password'])) {
|
||||
$adminAccountSafeCache->record();
|
||||
return '账号不存在';
|
||||
@@ -143,4 +150,4 @@ class LoginValidate extends BaseValidate
|
||||
return in_array($flag, ['1', 'true', 'yes', 'on'], true);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\model\iam;
|
||||
|
||||
use app\common\model\BaseModel;
|
||||
|
||||
class IamRevocationEvent extends BaseModel
|
||||
{
|
||||
protected $json = ['payload'];
|
||||
protected $jsonAssoc = true;
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\service\iam;
|
||||
|
||||
use app\adminapi\logic\LoginLogic;
|
||||
use app\adminapi\service\AdminTokenService;
|
||||
use app\common\model\auth\Admin;
|
||||
use app\common\model\auth\AdminRole;
|
||||
use app\common\service\FileService;
|
||||
use RuntimeException;
|
||||
use think\facade\Config;
|
||||
use think\facade\Db;
|
||||
|
||||
class IamAdminIdentityService
|
||||
{
|
||||
private array $config;
|
||||
private ?IamHubClient $client;
|
||||
|
||||
public function __construct(?array $config = null, ?IamHubClient $client = null)
|
||||
{
|
||||
$this->config = $config ?? (array) config('iam_hub');
|
||||
$this->client = $client;
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
public function login(array $claims, int $terminal): array
|
||||
{
|
||||
$subject = trim((string) ($claims['sub'] ?? ''));
|
||||
if ($subject === '') {
|
||||
throw new RuntimeException('IAM subject is missing');
|
||||
}
|
||||
$client = $this->client ?? new IamHubClient($this->config);
|
||||
$context = $client->provisioningContext($subject);
|
||||
$employee = $context['employee'] ?? null;
|
||||
if (!is_array($employee) || !hash_equals($subject, (string) ($employee['oidcSubject'] ?? ''))) {
|
||||
throw new RuntimeException('IAM provisioning context does not match the login identity');
|
||||
}
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
$admin = Admin::where('iam_subject', '=', $subject)->find();
|
||||
$externalId = trim((string) ($context['externalAccountId'] ?? ''));
|
||||
if (!$admin && ctype_digit($externalId) && (int) $externalId > 0) {
|
||||
$admin = Admin::find((int) $externalId);
|
||||
if ($admin && trim((string) ($admin->iam_subject ?? '')) !== ''
|
||||
&& !hash_equals($subject, (string) $admin->iam_subject)) {
|
||||
throw new RuntimeException('本地管理员账号已绑定其他统一身份');
|
||||
}
|
||||
}
|
||||
|
||||
if (!$admin) {
|
||||
$account = self::stableAccount($subject);
|
||||
$collision = Admin::where('account', '=', $account)->find();
|
||||
if ($collision) {
|
||||
throw new RuntimeException('统一身份账号映射冲突');
|
||||
}
|
||||
$admin = Admin::create([
|
||||
'account' => $account,
|
||||
'name' => (string) ($employee['displayName'] ?? $claims['name'] ?? $claims['preferred_username'] ?? $account),
|
||||
'password' => '',
|
||||
'avatar' => Config::get('project.default_image.admin_avatar'),
|
||||
'root' => 0,
|
||||
'disable' => 0,
|
||||
'multipoint_login' => 1,
|
||||
'is_paw' => 1,
|
||||
'iam_subject' => $subject,
|
||||
'iam_managed' => 1,
|
||||
'iam_revoked_at' => 0,
|
||||
]);
|
||||
foreach ($this->defaultRoleIds() as $roleId) {
|
||||
AdminRole::create(['admin_id' => $admin->id, 'role_id' => $roleId]);
|
||||
}
|
||||
} else {
|
||||
if ((int) ($admin->disable ?? 0) === 1 && (int) ($admin->iam_revoked_at ?? 0) === 0) {
|
||||
throw new RuntimeException('本地管理员账号已被平台禁用');
|
||||
}
|
||||
$admin->iam_subject = $subject;
|
||||
$admin->iam_managed = 1;
|
||||
if ((int) ($admin->iam_revoked_at ?? 0) > 0) {
|
||||
$admin->disable = 0;
|
||||
$admin->iam_revoked_at = 0;
|
||||
}
|
||||
$admin->save();
|
||||
}
|
||||
$admin->login_time = time();
|
||||
$admin->login_ip = request()->ip();
|
||||
$admin->save();
|
||||
Db::commit();
|
||||
} catch (\Throwable $error) {
|
||||
Db::rollback();
|
||||
throw $error;
|
||||
}
|
||||
|
||||
$client->bindAccount($subject, (int) $admin->id);
|
||||
$adminInfo = AdminTokenService::setToken($admin->id, $terminal, $admin->multipoint_login);
|
||||
if (!is_array($adminInfo) || empty($adminInfo['token'])) {
|
||||
throw new RuntimeException('本地管理员会话创建失败');
|
||||
}
|
||||
$avatar = $admin->avatar ?: Config::get('project.default_image.admin_avatar');
|
||||
return [
|
||||
'name' => $adminInfo['name'],
|
||||
'avatar' => FileService::getFileUrl($avatar),
|
||||
'role_name' => $adminInfo['role_name'],
|
||||
'token' => $adminInfo['token'],
|
||||
'is_paw' => 1,
|
||||
'need_bind_work_wechat' => LoginLogic::adminMustBindWorkWechat([
|
||||
'root' => (int) ($admin->root ?? 0),
|
||||
'work_wechat_userid' => (string) ($admin->work_wechat_userid ?? ''),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
public static function stableAccount(string $subject): string
|
||||
{
|
||||
return 'iam_' . substr(hash('sha256', $subject), 0, 20);
|
||||
}
|
||||
|
||||
/** @return int[] */
|
||||
private function defaultRoleIds(): array
|
||||
{
|
||||
$values = explode(',', (string) ($this->config['default_role_ids'] ?? ''));
|
||||
$roles = array_values(array_unique(array_filter(
|
||||
array_map('intval', $values),
|
||||
static fn (int $value): bool => $value > 0
|
||||
)));
|
||||
sort($roles, SORT_NUMERIC);
|
||||
return $roles;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\service\iam;
|
||||
|
||||
use think\facade\Cache;
|
||||
|
||||
class IamExchangeService
|
||||
{
|
||||
private int $ttlSeconds;
|
||||
|
||||
public function __construct(int $ttlSeconds = 60)
|
||||
{
|
||||
$this->ttlSeconds = $ttlSeconds;
|
||||
}
|
||||
|
||||
public function store(array $loginResult): string
|
||||
{
|
||||
$code = IamSecurity::base64UrlEncode(random_bytes(32));
|
||||
Cache::set($this->key($code), $loginResult, max(30, $this->ttlSeconds));
|
||||
return $code;
|
||||
}
|
||||
|
||||
public function consume(string $code): ?array
|
||||
{
|
||||
if (strlen($code) < 20 || strlen($code) > 200) {
|
||||
return null;
|
||||
}
|
||||
$value = Cache::pull($this->key($code));
|
||||
return is_array($value) ? $value : null;
|
||||
}
|
||||
|
||||
private function key(string $code): string
|
||||
{
|
||||
return 'iam_login_exchange_' . hash('sha256', $code);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\service\iam;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class IamHttpClient
|
||||
{
|
||||
/** @var callable|null */
|
||||
private $transport;
|
||||
private int $timeoutSeconds;
|
||||
|
||||
public function __construct(?callable $transport = null, int $timeoutSeconds = 10)
|
||||
{
|
||||
$this->transport = $transport;
|
||||
$this->timeoutSeconds = $timeoutSeconds;
|
||||
}
|
||||
|
||||
/** @return array{status:int,body:array<string,mixed>} */
|
||||
public function json(string $method, string $url, array $headers = [], ?array $json = null): array
|
||||
{
|
||||
$headers[] = 'Accept: application/json';
|
||||
$body = null;
|
||||
if ($json !== null) {
|
||||
$headers[] = 'Content-Type: application/json';
|
||||
$body = json_encode($json, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
|
||||
}
|
||||
|
||||
return $this->send($method, $url, $headers, $body);
|
||||
}
|
||||
|
||||
/** @return array{status:int,body:array<string,mixed>} */
|
||||
public function form(string $url, array $form): array
|
||||
{
|
||||
return $this->send(
|
||||
'POST',
|
||||
$url,
|
||||
['Accept: application/json', 'Content-Type: application/x-www-form-urlencoded'],
|
||||
http_build_query($form, '', '&', PHP_QUERY_RFC3986)
|
||||
);
|
||||
}
|
||||
|
||||
/** @return array{status:int,body:array<string,mixed>} */
|
||||
private function send(string $method, string $url, array $headers, ?string $body): array
|
||||
{
|
||||
if ($this->transport !== null) {
|
||||
$result = ($this->transport)($method, $url, $headers, $body);
|
||||
if (!is_array($result) || !isset($result['status'], $result['body'])) {
|
||||
throw new RuntimeException('IAM HTTP transport returned an invalid response');
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
$curl = curl_init($url);
|
||||
if ($curl === false) {
|
||||
throw new RuntimeException('Unable to initialize IAM HTTP request');
|
||||
}
|
||||
curl_setopt_array($curl, [
|
||||
CURLOPT_CUSTOMREQUEST => strtoupper($method),
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_FOLLOWLOCATION => false,
|
||||
CURLOPT_CONNECTTIMEOUT => min(5, $this->timeoutSeconds),
|
||||
CURLOPT_TIMEOUT => $this->timeoutSeconds,
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
]);
|
||||
if ($body !== null) {
|
||||
curl_setopt($curl, CURLOPT_POSTFIELDS, $body);
|
||||
}
|
||||
$raw = curl_exec($curl);
|
||||
$status = (int) curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
|
||||
$error = curl_error($curl);
|
||||
curl_close($curl);
|
||||
if ($raw === false) {
|
||||
throw new RuntimeException('IAM HTTP request failed: ' . $error);
|
||||
}
|
||||
$decoded = json_decode((string) $raw, true);
|
||||
return [
|
||||
'status' => $status,
|
||||
'body' => is_array($decoded) ? $decoded : [],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\service\iam;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class IamHubClient
|
||||
{
|
||||
private array $config;
|
||||
private IamHttpClient $http;
|
||||
|
||||
public function __construct(?array $config = null, ?IamHttpClient $http = null)
|
||||
{
|
||||
$this->config = $config ?? (array) config('iam_hub');
|
||||
$this->http = $http ?? new IamHttpClient(null, (int) ($this->config['http_timeout_seconds'] ?? 10));
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
public function provisioningContext(string $subject): array
|
||||
{
|
||||
return $this->request(
|
||||
'GET',
|
||||
sprintf(
|
||||
'%s/internal/v1/applications/%s/employees/%s/provisioning-context',
|
||||
$this->baseUrl(),
|
||||
rawurlencode($this->applicationId()),
|
||||
rawurlencode($subject)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function bindAccount(string $subject, int $adminId): void
|
||||
{
|
||||
$this->request(
|
||||
'POST',
|
||||
sprintf(
|
||||
'%s/internal/v1/applications/%s/bindings',
|
||||
$this->baseUrl(),
|
||||
rawurlencode($this->applicationId())
|
||||
),
|
||||
[
|
||||
'employeeSubject' => $subject,
|
||||
'externalAccountId' => (string) $adminId,
|
||||
'verificationMethod' => 'oidc-lazy-provision',
|
||||
],
|
||||
$this->bindingHeaders($subject, $adminId)
|
||||
);
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
private function request(string $method, string $url, ?array $body = null, array $extraHeaders = []): array
|
||||
{
|
||||
$token = trim((string) ($this->config['application_token'] ?? ''));
|
||||
if ($token === '') {
|
||||
throw new RuntimeException('IAM Hub application credential is not configured');
|
||||
}
|
||||
$result = $this->http->json(
|
||||
$method,
|
||||
$url,
|
||||
array_merge(['Authorization: Bearer ' . $token], $extraHeaders),
|
||||
$body
|
||||
);
|
||||
if ($result['status'] < 200 || $result['status'] >= 300) {
|
||||
$message = (string) ($result['body']['error'] ?? $result['body']['message'] ?? 'IAM Hub request failed');
|
||||
throw new RuntimeException($message . ' (HTTP ' . $result['status'] . ')');
|
||||
}
|
||||
return $result['body'];
|
||||
}
|
||||
|
||||
private function baseUrl(): string
|
||||
{
|
||||
return rtrim((string) ($this->config['base_url'] ?? ''), '/');
|
||||
}
|
||||
|
||||
private function applicationId(): string
|
||||
{
|
||||
return (string) ($this->config['application_id'] ?? 'zyt');
|
||||
}
|
||||
|
||||
private function bindingHeaders(string $subject, int $adminId): array
|
||||
{
|
||||
$requestId = hash('sha256', $this->applicationId() . ':' . $subject . ':' . $adminId);
|
||||
return ['X-Request-ID: ' . $requestId, 'Idempotency-Key: ' . $requestId];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\service\iam;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class IamHubException extends RuntimeException
|
||||
{
|
||||
private int $httpStatus;
|
||||
|
||||
public function __construct(string $message, int $httpStatus = 400)
|
||||
{
|
||||
parent::__construct($message);
|
||||
$this->httpStatus = $httpStatus;
|
||||
}
|
||||
|
||||
public function httpStatus(): int
|
||||
{
|
||||
return $this->httpStatus;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\service\iam;
|
||||
|
||||
class IamJwtVerifier
|
||||
{
|
||||
private string $issuer;
|
||||
private string $clientId;
|
||||
private int $clockSkewSeconds;
|
||||
|
||||
public function __construct(
|
||||
string $issuer,
|
||||
string $clientId,
|
||||
int $clockSkewSeconds = 30
|
||||
) {
|
||||
$this->issuer = $issuer;
|
||||
$this->clientId = $clientId;
|
||||
$this->clockSkewSeconds = $clockSkewSeconds;
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
public function verify(string $jwt, string $accessToken, string $nonce, array $jwks): array
|
||||
{
|
||||
$parts = explode('.', $jwt);
|
||||
if (count($parts) !== 3) {
|
||||
throw new IamHubException('Invalid ID token');
|
||||
}
|
||||
[$encodedHeader, $encodedPayload, $encodedSignature] = $parts;
|
||||
$header = $this->decodeJson($encodedHeader);
|
||||
$claims = $this->decodeJson($encodedPayload);
|
||||
if (($header['alg'] ?? '') !== 'RS256' || trim((string) ($header['kid'] ?? '')) === '') {
|
||||
throw new IamHubException('Unsupported ID token algorithm');
|
||||
}
|
||||
$key = $this->matchingKey((string) $header['kid'], $jwks);
|
||||
$verified = openssl_verify(
|
||||
$encodedHeader . '.' . $encodedPayload,
|
||||
IamSecurity::base64UrlDecode($encodedSignature),
|
||||
$this->jwkToPem($key),
|
||||
OPENSSL_ALGO_SHA256
|
||||
);
|
||||
if ($verified !== 1) {
|
||||
throw new IamHubException('Invalid ID token signature');
|
||||
}
|
||||
|
||||
$now = time();
|
||||
if (!hash_equals($this->issuer, (string) ($claims['iss'] ?? ''))) {
|
||||
throw new IamHubException('Invalid ID token issuer');
|
||||
}
|
||||
$audience = $claims['aud'] ?? [];
|
||||
$audiences = is_array($audience) ? $audience : [$audience];
|
||||
if (!in_array($this->clientId, $audiences, true)) {
|
||||
throw new IamHubException('Invalid ID token audience');
|
||||
}
|
||||
if ((int) ($claims['exp'] ?? 0) < $now - $this->clockSkewSeconds) {
|
||||
throw new IamHubException('Expired ID token');
|
||||
}
|
||||
if ((int) ($claims['iat'] ?? 0) > $now + $this->clockSkewSeconds) {
|
||||
throw new IamHubException('Invalid ID token issue time');
|
||||
}
|
||||
if ($nonce === '' || !hash_equals($nonce, (string) ($claims['nonce'] ?? ''))) {
|
||||
throw new IamHubException('Invalid ID token nonce');
|
||||
}
|
||||
if (trim((string) ($claims['sub'] ?? '')) === '') {
|
||||
throw new IamHubException('ID token subject is missing');
|
||||
}
|
||||
if (isset($claims['at_hash'])) {
|
||||
$expected = IamSecurity::base64UrlEncode(substr(hash('sha256', $accessToken, true), 0, 16));
|
||||
if (!hash_equals($expected, (string) $claims['at_hash'])) {
|
||||
throw new IamHubException('Invalid access token hash');
|
||||
}
|
||||
}
|
||||
return $claims;
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
private function decodeJson(string $value): array
|
||||
{
|
||||
$decoded = json_decode(IamSecurity::base64UrlDecode($value), true);
|
||||
if (!is_array($decoded)) {
|
||||
throw new IamHubException('Invalid ID token JSON');
|
||||
}
|
||||
return $decoded;
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
private function matchingKey(string $kid, array $jwks): array
|
||||
{
|
||||
foreach (($jwks['keys'] ?? []) as $key) {
|
||||
if (is_array($key) && ($key['kid'] ?? '') === $kid && ($key['kty'] ?? '') === 'RSA') {
|
||||
return $key;
|
||||
}
|
||||
}
|
||||
throw new IamHubException('ID token signing key was not found');
|
||||
}
|
||||
|
||||
private function jwkToPem(array $key): string
|
||||
{
|
||||
$modulus = IamSecurity::base64UrlDecode((string) ($key['n'] ?? ''));
|
||||
$exponent = IamSecurity::base64UrlDecode((string) ($key['e'] ?? ''));
|
||||
if ($modulus === '' || $exponent === '') {
|
||||
throw new IamHubException('Invalid RSA signing key');
|
||||
}
|
||||
$rsaKey = $this->asn1Sequence($this->asn1Integer($modulus) . $this->asn1Integer($exponent));
|
||||
$algorithm = hex2bin('300d06092a864886f70d0101010500');
|
||||
$publicKey = $this->asn1Sequence($algorithm . "\x03" . $this->asn1Length(strlen($rsaKey) + 1) . "\x00" . $rsaKey);
|
||||
return "-----BEGIN PUBLIC KEY-----\n"
|
||||
. chunk_split(base64_encode($publicKey), 64, "\n")
|
||||
. "-----END PUBLIC KEY-----\n";
|
||||
}
|
||||
|
||||
private function asn1Integer(string $value): string
|
||||
{
|
||||
$value = ltrim($value, "\x00") ?: "\x00";
|
||||
if ((ord($value[0]) & 0x80) !== 0) {
|
||||
$value = "\x00" . $value;
|
||||
}
|
||||
return "\x02" . $this->asn1Length(strlen($value)) . $value;
|
||||
}
|
||||
|
||||
private function asn1Sequence(string $value): string
|
||||
{
|
||||
return "\x30" . $this->asn1Length(strlen($value)) . $value;
|
||||
}
|
||||
|
||||
private function asn1Length(int $length): string
|
||||
{
|
||||
if ($length < 128) {
|
||||
return chr($length);
|
||||
}
|
||||
$bytes = ltrim(pack('N', $length), "\x00");
|
||||
return chr(0x80 | strlen($bytes)) . $bytes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\service\iam;
|
||||
|
||||
use RuntimeException;
|
||||
use think\facade\Cache;
|
||||
|
||||
class IamOidcService
|
||||
{
|
||||
private array $config;
|
||||
private IamHttpClient $http;
|
||||
|
||||
public function __construct(?array $config = null, ?IamHttpClient $http = null)
|
||||
{
|
||||
$this->config = $config ?? (array) config('iam_hub');
|
||||
$this->http = $http ?? new IamHttpClient(null, (int) ($this->config['http_timeout_seconds'] ?? 10));
|
||||
}
|
||||
|
||||
public function enabled(): bool
|
||||
{
|
||||
return (bool) ($this->config['enabled'] ?? false)
|
||||
&& trim((string) ($this->config['application_token'] ?? '')) !== ''
|
||||
&& trim((string) ($this->config['oidc_client_secret'] ?? '')) !== ''
|
||||
&& trim((string) ($this->config['oidc_redirect_uri'] ?? '')) !== '';
|
||||
}
|
||||
|
||||
public function begin(): string
|
||||
{
|
||||
$this->assertEnabled();
|
||||
$state = IamSecurity::base64UrlEncode(random_bytes(32));
|
||||
$nonce = IamSecurity::base64UrlEncode(random_bytes(32));
|
||||
$verifier = IamSecurity::base64UrlEncode(random_bytes(64));
|
||||
$challenge = IamSecurity::base64UrlEncode(hash('sha256', $verifier, true));
|
||||
Cache::set($this->stateKey($state), [
|
||||
'nonce' => $nonce,
|
||||
'verifier' => $verifier,
|
||||
], (int) ($this->config['state_ttl_seconds'] ?? 300));
|
||||
|
||||
return $this->authorizationUrl($state, $nonce, $challenge);
|
||||
}
|
||||
|
||||
public function authorizationUrl(string $state, string $nonce, string $challenge): string
|
||||
{
|
||||
return rtrim((string) ($this->config['oidc_issuer'] ?? ''), '/')
|
||||
. '/protocol/openid-connect/auth?'
|
||||
. http_build_query([
|
||||
'client_id' => (string) ($this->config['oidc_client_id'] ?? ''),
|
||||
'redirect_uri' => (string) ($this->config['oidc_redirect_uri'] ?? ''),
|
||||
'response_type' => 'code',
|
||||
'scope' => 'openid profile email',
|
||||
'state' => $state,
|
||||
'nonce' => $nonce,
|
||||
'code_challenge' => $challenge,
|
||||
'code_challenge_method' => 'S256',
|
||||
], '', '&', PHP_QUERY_RFC3986);
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
public function callback(string $code, string $state): array
|
||||
{
|
||||
$this->assertEnabled();
|
||||
$stateData = Cache::pull($this->stateKey($state));
|
||||
if (!is_array($stateData) || empty($stateData['nonce']) || empty($stateData['verifier'])) {
|
||||
throw new IamHubException('统一身份登录状态无效或已过期');
|
||||
}
|
||||
|
||||
$tokenResult = $this->http->form($this->internalBaseUrl() . '/protocol/openid-connect/token', [
|
||||
'grant_type' => 'authorization_code',
|
||||
'client_id' => (string) ($this->config['oidc_client_id'] ?? ''),
|
||||
'client_secret' => (string) ($this->config['oidc_client_secret'] ?? ''),
|
||||
'redirect_uri' => (string) ($this->config['oidc_redirect_uri'] ?? ''),
|
||||
'code' => $code,
|
||||
'code_verifier' => (string) $stateData['verifier'],
|
||||
]);
|
||||
if ($tokenResult['status'] !== 200) {
|
||||
throw new IamHubException('统一身份授权码交换失败', 502);
|
||||
}
|
||||
$idToken = (string) ($tokenResult['body']['id_token'] ?? '');
|
||||
$accessToken = (string) ($tokenResult['body']['access_token'] ?? '');
|
||||
if ($idToken === '' || $accessToken === '') {
|
||||
throw new IamHubException('统一身份令牌响应不完整', 502);
|
||||
}
|
||||
$jwksResult = $this->http->json('GET', $this->internalBaseUrl() . '/protocol/openid-connect/certs');
|
||||
if ($jwksResult['status'] !== 200) {
|
||||
throw new IamHubException('统一身份签名密钥获取失败', 502);
|
||||
}
|
||||
|
||||
return (new IamJwtVerifier(
|
||||
(string) ($this->config['oidc_issuer'] ?? ''),
|
||||
(string) ($this->config['oidc_client_id'] ?? '')
|
||||
))->verify($idToken, $accessToken, (string) $stateData['nonce'], $jwksResult['body']);
|
||||
}
|
||||
|
||||
private function assertEnabled(): void
|
||||
{
|
||||
if (!$this->enabled()) {
|
||||
throw new IamHubException('统一身份登录尚未配置', 503);
|
||||
}
|
||||
}
|
||||
|
||||
private function internalBaseUrl(): string
|
||||
{
|
||||
$internal = trim((string) ($this->config['oidc_internal_base_url'] ?? ''));
|
||||
return rtrim($internal !== '' ? $internal : (string) ($this->config['oidc_issuer'] ?? ''), '/');
|
||||
}
|
||||
|
||||
private function stateKey(string $state): string
|
||||
{
|
||||
return 'iam_oidc_state_' . hash('sha256', $state);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\service\iam;
|
||||
|
||||
use app\adminapi\logic\auth\AdminLogic;
|
||||
use app\common\cache\AdminAuthCache;
|
||||
use app\common\model\auth\Admin;
|
||||
use app\common\model\auth\AdminSession;
|
||||
use app\common\model\iam\IamRevocationEvent;
|
||||
use think\facade\Db;
|
||||
|
||||
class IamRevocationService
|
||||
{
|
||||
private array $config;
|
||||
|
||||
public function __construct(?array $config = null)
|
||||
{
|
||||
$this->config = $config ?? (array) config('iam_hub');
|
||||
}
|
||||
|
||||
public function apply(string $body, string $signature, string $eventHeader): void
|
||||
{
|
||||
if (!(bool) ($this->config['enabled'] ?? false)) {
|
||||
throw new IamHubException('IAM Hub integration is disabled', 503);
|
||||
}
|
||||
$secret = (string) ($this->config['revocation_secret'] ?? '');
|
||||
if (!IamSecurity::verifySignature($secret, $body, $signature)) {
|
||||
throw new IamHubException('invalid IAM Hub signature', 401);
|
||||
}
|
||||
$event = json_decode($body, true);
|
||||
if (!is_array($event)) {
|
||||
throw new IamHubException('invalid IAM Hub event JSON');
|
||||
}
|
||||
$eventId = trim((string) ($event['id'] ?? ''));
|
||||
$subject = trim((string) ($event['oidcSubject'] ?? ''));
|
||||
$applications = $event['applicationIds'] ?? [];
|
||||
if ($eventId === '' || $subject === '' || !is_array($applications)) {
|
||||
throw new IamHubException('invalid IAM Hub event');
|
||||
}
|
||||
if (!hash_equals($eventId, trim($eventHeader))) {
|
||||
throw new IamHubException('IAM Hub event ID header mismatch');
|
||||
}
|
||||
if (!in_array((string) ($this->config['application_id'] ?? 'zyt'), $applications, true)) {
|
||||
throw new IamHubException('IAM Hub event is not addressed to this application', 403);
|
||||
}
|
||||
if (IamRevocationEvent::where('event_id', '=', $eventId)->find()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
$admin = Admin::where('iam_subject', '=', $subject)->find();
|
||||
$status = 'admin_not_found';
|
||||
$adminId = 0;
|
||||
if ($admin) {
|
||||
$adminId = (int) $admin->id;
|
||||
$admin->disable = 1;
|
||||
$admin->iam_revoked_at = time();
|
||||
$admin->save();
|
||||
$sessions = AdminSession::where('admin_id', '=', $adminId)->select()->toArray();
|
||||
foreach ($sessions as $session) {
|
||||
AdminLogic::expireToken((string) $session['token']);
|
||||
}
|
||||
(new AdminAuthCache($adminId))->clearAuthCache();
|
||||
$status = 'applied';
|
||||
}
|
||||
IamRevocationEvent::create([
|
||||
'event_id' => $eventId,
|
||||
'iam_subject' => $subject,
|
||||
'admin_id' => $adminId,
|
||||
'status' => $status,
|
||||
'reason' => (string) ($event['reason'] ?? ''),
|
||||
'payload' => $event,
|
||||
]);
|
||||
Db::commit();
|
||||
} catch (\Throwable $error) {
|
||||
Db::rollback();
|
||||
throw $error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\service\iam;
|
||||
|
||||
class IamSecurity
|
||||
{
|
||||
public static function signatureFor(string $secret, string $body): string
|
||||
{
|
||||
return 'sha256=' . hash_hmac('sha256', $body, $secret);
|
||||
}
|
||||
|
||||
public static function verifySignature(string $secret, string $body, string $provided): bool
|
||||
{
|
||||
return $secret !== '' && hash_equals(self::signatureFor($secret, $body), trim($provided));
|
||||
}
|
||||
|
||||
public static function base64UrlEncode(string $value): string
|
||||
{
|
||||
return rtrim(strtr(base64_encode($value), '+/', '-_'), '=');
|
||||
}
|
||||
|
||||
public static function base64UrlDecode(string $value): string
|
||||
{
|
||||
$padding = (4 - strlen($value) % 4) % 4;
|
||||
$decoded = base64_decode(strtr($value . str_repeat('=', $padding), '-_', '+/'), true);
|
||||
if ($decoded === false) {
|
||||
throw new IamHubException('Invalid base64url value');
|
||||
}
|
||||
return $decoded;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'enabled' => filter_var(env('iam_hub.enabled', false), FILTER_VALIDATE_BOOL),
|
||||
'base_url' => rtrim((string) env('iam_hub.base_url', 'http://iam-hub-api:8080'), '/'),
|
||||
'application_id' => (string) env('iam_hub.application_id', 'zyt'),
|
||||
'application_token' => (string) env('iam_hub.application_token', ''),
|
||||
'revocation_secret' => (string) env('iam_hub.revocation_secret', ''),
|
||||
'oidc_issuer' => rtrim((string) env('iam_hub.oidc_issuer', 'http://localhost:8081/realms/iam-hub'), '/'),
|
||||
'oidc_internal_base_url' => rtrim((string) env('iam_hub.oidc_internal_base_url', ''), '/'),
|
||||
'oidc_client_id' => (string) env('iam_hub.oidc_client_id', 'zyt'),
|
||||
'oidc_client_secret' => (string) env('iam_hub.oidc_client_secret', ''),
|
||||
'oidc_redirect_uri' => (string) env('iam_hub.oidc_redirect_uri', ''),
|
||||
'login_success_url' => (string) env('iam_hub.login_success_url', '/admin/login'),
|
||||
'default_role_ids' => (string) env('iam_hub.default_role_ids', ''),
|
||||
'state_ttl_seconds' => max(60, (int) env('iam_hub.state_ttl_seconds', 300)),
|
||||
'exchange_ttl_seconds' => max(30, (int) env('iam_hub.exchange_ttl_seconds', 60)),
|
||||
'http_timeout_seconds' => max(2, (int) env('iam_hub.http_timeout_seconds', 10)),
|
||||
];
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
import t from"./error-DJTRJcDD.js";import{o,q as r,r as a,v as n,D as c,s}from"./.pnpm-BSw4l71J.js";import"./index-0obBWZiZ.js";const p="/admin/assets/no_perms-jDxcYpYC.png",i={class:"error404"},x=o({__name:"403",setup(m){return(_,e)=>(r(),a("div",i,[n(t,{code:"403",title:"您的账号权限不足,请联系管理员添加权限!","show-btn":!1},{content:c(()=>[...e[0]||(e[0]=[s("div",{class:"flex justify-center"},[s("img",{class:"w-[150px] h-[150px]",src:p,alt:""})],-1)])]),_:1})]))}});export{x as default};
|
||||
@@ -0,0 +1 @@
|
||||
import r from"./error-BU0Zs3ny.js";import{f as p,ak as i,I as m,a as e,aN as s,J as o}from"./@vue/runtime-core-C6bnekPw.js";import"./element-plus-DGP1XOrU.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/shared-mAAVTE9n.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@element-plus/icons-vue-CbYn5zW9.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./vue-router-QlpZ4wdW.js";import"./index-fRv3NTFQ.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const a="/admin/assets/no_perms-jDxcYpYC.png",n={class:"error404"},W=p({__name:"403",setup(c){return(_,t)=>(i(),m("div",n,[e(r,{code:"403",title:"您的账号权限不足,请联系管理员添加权限!","show-btn":!1},{content:s(()=>[...t[0]||(t[0]=[o("div",{class:"flex justify-center"},[o("img",{class:"w-[150px] h-[150px]",src:a,alt:""})],-1)])]),_:1})]))}});export{W as default};
|
||||
@@ -0,0 +1 @@
|
||||
import o from"./error-BU0Zs3ny.js";import{f as r,ak as t,I as m,a as p}from"./@vue/runtime-core-C6bnekPw.js";import"./element-plus-DGP1XOrU.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/shared-mAAVTE9n.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@element-plus/icons-vue-CbYn5zW9.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./vue-router-QlpZ4wdW.js";import"./index-fRv3NTFQ.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const i={class:"error404"},T=r({__name:"404",setup(e){return(a,s)=>(t(),m("div",i,[p(o,{code:"404",title:"哎呀,出错了!您访问的页面不存在…"})]))}});export{T as default};
|
||||
@@ -1 +0,0 @@
|
||||
import e from"./error-DJTRJcDD.js";import{o,q as r,r as t,v as s}from"./.pnpm-BSw4l71J.js";import"./index-0obBWZiZ.js";const a={class:"error404"},d=o({__name:"404",setup(c){return(n,_)=>(r(),t("div",a,[s(e,{code:"404",title:"哎呀,出错了!您访问的页面不存在…"})]))}});export{d as default};
|
||||
@@ -0,0 +1 @@
|
||||
function a(e){"@babel/helpers - typeof";return a=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},a(e)}function c(e,t,r,n,f,y,i){try{var u=e[y](i),o=u.value}catch(l){return void r(l)}u.done?t(o):Promise.resolve(o).then(n,f)}function p(e){return function(){var t=this,r=arguments;return new Promise(function(n,f){var y=e.apply(t,r);function i(o){c(y,n,f,i,u,"next",o)}function u(o){c(y,n,f,i,u,"throw",o)}i(void 0)})}}function b(e,t){if(a(e)!="object"||!e)return e;var r=e[Symbol.toPrimitive];if(r!==void 0){var n=r.call(e,t);if(a(n)!="object")return n;throw new TypeError("@@toPrimitive must return a primitive value.")}return(t==="string"?String:Number)(e)}function m(e){var t=b(e,"string");return a(t)=="symbol"?t:t+""}function s(e,t,r){return(t=m(t))in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}export{a as _,p as a,s as b};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
import{H as u}from"../highlight.js-Bxt7hFFy.js";import{f as c,i as g,w as s,A as n}from"../@vue/runtime-core-C6bnekPw.js";import{n as h}from"../@vue/reactivity-DiY1c2vO.js";var i=c({props:{code:{type:String,required:!0},language:{type:String,default:""},autodetect:{type:Boolean,default:!0},ignoreIllegals:{type:Boolean,default:!0}},setup:function(e){var t=h(e.language);s((function(){return e.language}),(function(a){t.value=a}));var r=n((function(){return e.autodetect||!t.value})),o=n((function(){return!r.value&&!u.getLanguage(t.value)}));return{className:n((function(){return o.value?"":"hljs "+t.value})),highlightedCode:n((function(){var a;if(o.value)return console.warn('The language "'+t.value+'" you specified could not be found.'),e.code.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'");if(r.value){var l=u.highlightAuto(e.code);return t.value=(a=l.language)!==null&&a!==void 0?a:"",l.value}return(l=u.highlight(e.code,{language:t.value,ignoreIllegals:e.ignoreIllegals})).value}))}},render:function(){return g("pre",{},[g("code",{class:this.className,innerHTML:this.highlightedCode})])}}),v={install:function(e){e.component("highlightjs",i)},component:i};export{v as o};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
import"./uikit-base-component-vue3-YgTqL4da.js";import{A as r}from"../tuikit-atomicx-vue3-Dln8Zi6e.js";import{f as s,ak as c,I as l,aq as m,G as u,at as i,H as p,A as d}from"../@vue/runtime-core-C6bnekPw.js";import{y as v}from"../@vue/reactivity-DiY1c2vO.js";import"./chat-uikit-engine-zx802ozq.js";const _=(t,o)=>{const a=t.__vccOpts||t;for(const[e,n]of o)a[e]=n;return a},f={key:0,class:"chat"},h=s({name:"Chat",__name:"Chat",props:{PlaceholderEmpty:{default:null}},setup(t){const{activeConversation:o}=r(),a=d(()=>{var e;return!((e=o.value)!=null&&e.conversationID)});return(e,n)=>v(o)?(c(),l("div",f,[m(e.$slots,"default",{},void 0,!0)])):a.value&&t.PlaceholderEmpty?(c(),u(i(t.PlaceholderEmpty),{key:1})):p("",!0)}}),A=_(h,[["__scopeId","data-v-1c9c77cd"]]);typeof window<"u"&&(window.__CHAT_ATOMICX_VUE3__={name:"@tencentcloud/chat-uikit-vue3",version:"4.5.4"},console.log("[@tencentcloud/chat-uikit-vue3] v4.5.4"));export{A as E};
|
||||
@@ -0,0 +1 @@
|
||||
.chat[data-v-1c9c77cd]{display:flex;flex-direction:column;min-width:0}.uikit-chat-header[data-v-a0c42ddc]{padding:14px 10px;height:64px;display:flex;justify-content:center;background-color:var(--bg-color-operate)}.uikit-chat-header__container[data-v-a0c42ddc]{padding:0 10px;flex-direction:row;align-items:center;justify-content:space-between}.uikit-chat-header__left[data-v-a0c42ddc]{flex:1 1 auto;display:flex;flex-direction:row;align-items:center}.uikit-chat-header__avatar[data-v-a0c42ddc]{margin-right:12px}.uikit-chat-header__info[data-v-a0c42ddc]{flex:1;display:flex;flex-direction:column;justify-content:center}.uikit-chat-header__title[data-v-a0c42ddc]{display:block;margin:0;font-size:16px;font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-color-primary)}.uikit-chat-header__typing-indicator[data-v-a0c42ddc]{font-size:12px;color:var(--text-color-secondary)}.uikit-chat-header__live[data-v-a0c42ddc]{margin-top:4px;font-size:12px;color:var(--text-color-secondary)}/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}:root{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}*,*:after,*:before{box-sizing:border-box}ul,li{list-style:none;padding:0;margin:0}picture,img,video,canvas,svg{display:block;max-width:100%}img{max-width:100%;height:auto;vertical-align:middle;image-rendering:-webkit-optimize-contrast;aspect-ratio:attr(width)/attr(height);display:inline-block;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}img:not([src],[srcset]){visibility:hidden}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
import{C as A,F as C,a as w,L as h,q as D,J as R,H as g,u as k,n as W}from"../@vue/reactivity-DiY1c2vO.js";import{w as b,b as x,n as L,g as F,$ as M,a5 as P}from"../@vue/runtime-core-C6bnekPw.js";function J(e){return A()?(C(e),!0):!1}const d=new WeakMap,z=(...e)=>{var t;const r=e[0],n=(t=F())==null?void 0:t.proxy;if(n==null&&!M())throw new Error("injectLocal must be called in setup");return n&&d.has(n)&&r in d.get(n)?d.get(n)[r]:P(...e)},B=typeof window<"u"&&typeof document<"u";typeof WorkerGlobalScope<"u"&&globalThis instanceof WorkerGlobalScope;const K=e=>typeof e<"u",I=Object.prototype.toString,Q=e=>I.call(e)==="[object Object]",m=()=>{};function j(e,t){function r(...n){return new Promise((a,o)=>{Promise.resolve(e(()=>t.apply(this,n),{fn:t,thisArg:this,args:n})).then(a).catch(o)})}return r}const S=e=>e();function V(...e){let t=0,r,n=!0,a=m,o,s,i,u,c;!w(e[0])&&typeof e[0]=="object"?{delay:s,trailing:i=!0,leading:u=!0,rejectOnCancel:c=!1}=e[0]:[s,i=!0,u=!0,c=!1]=e;const f=()=>{r&&(clearTimeout(r),r=void 0,a(),a=m)};return O=>{const l=h(s),v=Date.now()-t,p=()=>o=O();return f(),l<=0?(t=Date.now(),p()):(v>l&&(u||!n)?(t=Date.now(),p()):i&&(o=new Promise((y,T)=>{a=c?T:y,r=setTimeout(()=>{t=Date.now(),n=!0,y(p()),f()},Math.max(0,l-v))})),!u&&!r&&(r=setTimeout(()=>n=!0,l)),n=!1,o)}}function E(e=S,t={}){const{initialState:r="active"}=t,n=N(r==="active");function a(){n.value=!1}function o(){n.value=!0}const s=(...i)=>{n.value&&e(...i)};return{isActive:g(n),pause:a,resume:o,eventFilter:s}}function U(e){return e.endsWith("rem")?Number.parseFloat(e)*16:Number.parseFloat(e)}function G(e){return F()}function X(e){return Array.isArray(e)?e:[e]}function N(...e){if(e.length!==1)return R(...e);const t=e[0];return typeof t=="function"?g(k(()=>({get:t,set:m}))):W(t)}function Y(e,t=200,r=!1,n=!0,a=!1){return j(V(t,r,n,a),e)}function _(e,t,r={}){const{eventFilter:n=S,...a}=r;return b(e,j(n,t),a)}function Z(e,t,r={}){const{eventFilter:n,initialState:a="active",...o}=r,{eventFilter:s,pause:i,resume:u,isActive:c}=E(n,{initialState:a});return{stop:_(e,t,{...o,eventFilter:s}),pause:i,resume:u,isActive:c}}function ee(e,t=!0,r){G()?x(e,r):t?e():L(e)}function te(e=!1,t={}){const{truthyValue:r=!0,falsyValue:n=!1}=t,a=w(e),o=D(e);function s(i){if(arguments.length)return o.value=i,o.value;{const u=h(r);return o.value=o.value===u?h(n):u,o.value}}return a?s:[o,s]}function ne(e,t,r){return b(e,t,{...r,immediate:!0})}export{N as a,ee as b,Q as c,X as d,Z as e,z as f,K as g,Y as h,B as i,U as p,J as t,te as u,ne as w};
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
|
||||
import{i as C,Q as y,a as E}from"./editor-Cyf37SuL.js";import{ak as g,I as h,f as w,b as P,w as O,aJ as b}from"../@vue/runtime-core-C6bnekPw.js";import{n as d,t as $,q as F}from"../@vue/reactivity-DiY1c2vO.js";var B=Object.defineProperty,D=Object.defineProperties,j=Object.getOwnPropertyDescriptors,m=Object.getOwnPropertySymbols,H=Object.prototype.hasOwnProperty,S=Object.prototype.propertyIsEnumerable,_=(e,t,o)=>t in e?B(e,t,{enumerable:!0,configurable:!0,writable:!0,value:o}):e[t]=o,A=(e,t)=>{for(var o in t||(t={}))H.call(t,o)&&_(e,o,t[o]);if(m)for(var o of m(t))S.call(t,o)&&_(e,o,t[o]);return e},M=(e,t)=>D(e,j(t));function u(e){let t=`请使用 '@${e}' 事件,不要放在 props 中`;return t+=`
|
||||
Please use '@${e}' event instead of props`,t}var v=(e,t)=>{for(const[o,a]of t)e[o]=a;return e};const V=w({props:{mode:{type:String,default:"default"},defaultContent:{type:Array,default:[]},defaultHtml:{type:String,default:""},defaultConfig:{type:Object,default:{}},modelValue:{type:String,default:""}},setup(e,t){const o=d(null),a=F(null),i=d(""),s=()=>{if(!o.value)return;const f=$(e.defaultContent);C({selector:o.value,mode:e.mode,content:f||[],html:e.defaultHtml||e.modelValue||"",config:M(A({},e.defaultConfig),{onCreated(r){if(a.value=r,t.emit("onCreated",r),e.defaultConfig.onCreated){const n=u("onCreated");throw new Error(n)}},onChange(r){const n=r.getHtml();if(i.value=n,t.emit("update:modelValue",n),t.emit("onChange",r),e.defaultConfig.onChange){const l=u("onChange");throw new Error(l)}},onDestroyed(r){if(t.emit("onDestroyed",r),e.defaultConfig.onDestroyed){const n=u("onDestroyed");throw new Error(n)}},onMaxLength(r){if(t.emit("onMaxLength",r),e.defaultConfig.onMaxLength){const n=u("onMaxLength");throw new Error(n)}},onFocus(r){if(t.emit("onFocus",r),e.defaultConfig.onFocus){const n=u("onFocus");throw new Error(n)}},onBlur(r){if(t.emit("onBlur",r),e.defaultConfig.onBlur){const n=u("onBlur");throw new Error(n)}},customAlert(r,n){if(t.emit("customAlert",r,n),e.defaultConfig.customAlert){const l=u("customAlert");throw new Error(l)}},customPaste:(r,n)=>{if(e.defaultConfig.customPaste){const c=u("customPaste");throw new Error(c)}let l;return t.emit("customPaste",r,n,c=>{l=c}),l}})})};function p(f){const r=a.value;r!=null&&r.setHtml(f)}return P(()=>{s()}),O(()=>e.modelValue,f=>{f!==i.value&&p(f)}),{box:o}}}),I={ref:"box",style:{height:"100%"}};function L(e,t,o,a,i,s){return g(),h("div",I,null,512)}var J=v(V,[["render",L]]);const T=w({props:{editor:{type:Object},mode:{type:String,default:"default"},defaultConfig:{type:Object,default:{}}},setup(e){const t=d(null),o=a=>{if(t.value){if(a==null)throw new Error("Not found instance of Editor when create <Toolbar/> component");y.getToolbar(a)||E({editor:a,selector:t.value||"<div></div>",mode:e.mode,config:e.defaultConfig})}};return b(()=>{const{editor:a}=e;a!=null&&o(a)}),{selector:t}}}),R={ref:"selector"};function k(e,t,o,a,i,s){return g(),h("div",R,null,512)}var N=v(T,[["render",k]]);export{J as E,N as T};
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
import{o as C,R as D,q as d,r as I,ac as N,O as u,bj as B,D as o,v as l,bk as E,br as L,L as i,T as r,s as p,bi as R,M as w}from"./.pnpm-BSw4l71J.js";import{a as V}from"./doctor-v05-2hcw.js";import{m as A,_ as M}from"./index-0obBWZiZ.js";const P={class:"appointment-record-panel"},$={class:"cell-stack"},j={class:"font-medium"},q={class:"text-gray-500 text-sm"},O={class:"cell-stack"},F={class:"text-gray-500 text-sm"},G=C({__name:"AppointmentRecordPanel",props:{diagnosisId:{}},setup(v,{expose:y}){const f=v,m=w(!1),g=w([]),h=w({});function x(t){return t?String(t).length>=8?String(t).slice(0,5):t:""}function k(t){return t==="morning"?"上午":t==="afternoon"?"下午":t==="all"?"全天":t||"—"}function S(t){const n=t.channel_source??t.channels??"";if(n===""||n===null||n===void 0)return"—";const a=String(n),s=h.value[a]||a,c=String(t.channel_source_detail??"").trim();return c!==""?`${s}(${c})`:s}function T(t){return[...t].sort((n,a)=>{const s=String(n.appointment_date||""),c=String(a.appointment_date||"");if(s!==c)return c.localeCompare(s);const _=String(n.appointment_time||""),e=String(a.appointment_time||"");return _!==e?e.localeCompare(_):Number(a.id||0)-Number(n.id||0)})}const z=async()=>{try{const t=await A({type:"channels"}),n=((t==null?void 0:t.channels)||[]).filter(s=>s.status!==0),a={};for(const s of n)s.value!=null&&(a[String(s.value)]=s.name||String(s.value));h.value=a}catch{h.value={}}},b=async()=>{if(f.diagnosisId){m.value=!0;try{await z();const t=await V({patient_id:f.diagnosisId,diag_scope_relax:1,page_no:1,page_size:500}),n=(t==null?void 0:t.lists)||[];g.value=T(n)}catch(t){console.error(t),g.value=[]}finally{m.value=!1}}};return D(()=>f.diagnosisId,()=>{b()},{immediate:!0}),y({refresh:b}),(t,n)=>{const a=E,s=L,c=B,_=R;return d(),I("div",P,[N((d(),u(c,{data:g.value,border:"",stripe:"","empty-text":"暂无挂号记录"},{default:o(()=>[l(a,{label:"ID",prop:"id",width:"72",align:"center"}),l(a,{label:"状态",width:"100",align:"center"},{default:o(({row:e})=>[l(s,{type:e.status===1?"success":e.status===2?"info":e.status===3?"primary":"danger",size:"small",effect:"light"},{default:o(()=>[i(r(e.status_desc||"—"),1)]),_:2},1032,["type"])]),_:1}),l(a,{label:"患者(挂号人)","min-width":"150"},{default:o(({row:e})=>[p("div",$,[p("span",j,r(e.patient_name||"—"),1),p("span",q,r(e.patient_phone||""),1)])]),_:1}),l(a,{label:"挂号医生",prop:"doctor_name",width:"110","show-overflow-tooltip":""}),l(a,{label:"挂号助理",width:"110","show-overflow-tooltip":""},{default:o(({row:e})=>[i(r(e.assistant_name||"—"),1)]),_:1}),l(a,{label:"预约时间","min-width":"130"},{default:o(({row:e})=>[p("div",O,[p("span",null,r(e.appointment_date||"—"),1),p("span",F,r(x(e.appointment_time)),1)])]),_:1}),l(a,{label:"时段",width:"80",align:"center"},{default:o(({row:e})=>[i(r(k(e.period)),1)]),_:1}),l(a,{label:"类型",width:"100","show-overflow-tooltip":""},{default:o(({row:e})=>[i(r(e.appointment_type_desc||"—"),1)]),_:1}),l(a,{label:"渠道",width:"110","show-overflow-tooltip":""},{default:o(({row:e})=>[i(r(S(e)),1)]),_:1}),l(a,{label:"确认诊单",width:"92",align:"center"},{default:o(({row:e})=>[e.diagnosis_confirmed?(d(),u(s,{key:0,type:"success",size:"small",effect:"plain"},{default:o(()=>[...n[0]||(n[0]=[i("已确认",-1)])]),_:1})):(d(),u(s,{key:1,type:"warning",size:"small",effect:"plain"},{default:o(()=>[...n[1]||(n[1]=[i("未确认",-1)])]),_:1}))]),_:1}),l(a,{label:"开方",width:"80",align:"center"},{default:o(({row:e})=>[e.has_prescription?(d(),u(s,{key:0,type:"success",size:"small",effect:"plain"},{default:o(()=>[...n[2]||(n[2]=[i("已开方",-1)])]),_:1})):(d(),u(s,{key:1,type:"info",size:"small",effect:"plain"},{default:o(()=>[...n[3]||(n[3]=[i("未开方",-1)])]),_:1}))]),_:1}),l(a,{label:"备注",prop:"remark","min-width":"100","show-overflow-tooltip":""}),l(a,{label:"创建时间",width:"165",prop:"create_time"})]),_:1},8,["data"])),[[_,m.value]])])}}}),Q=M(G,[["__scopeId","data-v-4de87dfa"]]);export{Q as default};
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
.assign-log-col-hint[data-v-0a09e3d2]{margin-left:4px;vertical-align:middle;color:var(--el-text-color-secondary);cursor:help}
|
||||
@@ -1 +0,0 @@
|
||||
import{o as T,R as C,q as _,r as b,ac as D,O as h,bj as $,D as r,v as n,bk as L,L as c,T as d,br as k,s as E,a1 as A,w as P,u as B,ch as F,bi as M,M as w}from"./.pnpm-BSw4l71J.js";import{af as V}from"./tcm-DKwa3Njw.js";import{_ as q}from"./index-0obBWZiZ.js";const K={class:"assign-log-panel"},j={key:1,class:"text-gray-400"},z=T({__name:"AssignLogPanel",props:{diagnosisId:{}},setup(v,{expose:y}){const u=v,m=w(!1),p=w([]);function N(a){const e=a.related_po_creator_name;if(e!=null&&String(e).trim()!=="")return String(e);const t=Number(a.related_po_creator_id);return Number.isFinite(t)&&t>0?`ID:${t}`:"—"}function x(a){const e=a.related_po_create_time_text;if(e!=null&&String(e).trim()!=="")return String(e);const t=Number(a.related_po_create_time);if(!Number.isFinite(t)||t<=0)return"—";const s=new Date(t*1e3);if(Number.isNaN(s.getTime()))return"—";const o=l=>String(l).padStart(2,"0");return`${s.getFullYear()}-${o(s.getMonth()+1)}-${o(s.getDate())} ${o(s.getHours())}:${o(s.getMinutes())}:${o(s.getSeconds())}`}function f(a,e){const t=e==="from"?"from_assistant_name":"to_assistant_name",s=e==="from"?"from_assistant_id":"to_assistant_id",o=a[t];if(o!=null&&String(o).trim()!==""&&String(o)!=="—")return String(o);const l=Number(a[s]);return Number.isFinite(l)&&l>0?`ID:${l}`:"—"}const g=async()=>{if(u.diagnosisId){m.value=!0;try{const a=await V({id:u.diagnosisId}),e=Array.isArray(a)?a:[];p.value=e}catch(a){console.error(a),p.value=[]}finally{m.value=!1}}};return C(()=>u.diagnosisId,()=>{g()},{immediate:!0}),y({refresh:g}),(a,e)=>{const t=L,s=k,o=P,l=A,S=$,I=M;return _(),b("div",K,[D((_(),h(S,{data:p.value,border:"",stripe:"","empty-text":"暂无指派记录"},{default:r(()=>[n(t,{label:"操作时间",width:"175",prop:"create_time_text"}),n(t,{label:"原医助","min-width":"120"},{default:r(({row:i})=>[c(d(f(i,"from")),1)]),_:1}),n(t,{label:"新医助","min-width":"120"},{default:r(({row:i})=>[c(d(f(i,"to")),1)]),_:1}),n(t,{label:"继承",width:"72",align:"center"},{default:r(({row:i})=>[Number(i.is_inherit)===1?(_(),h(s,{key:0,type:"success",size:"small"},{default:r(()=>[...e[0]||(e[0]=[c("是",-1)])]),_:1})):(_(),b("span",j,"否"))]),_:1}),n(t,{label:"快照·业务单创建人","min-width":"130","show-overflow-tooltip":""},{default:r(({row:i})=>[c(d(N(i)),1)]),_:1}),n(t,{label:"快照·业务单创建时间",width:"190"},{header:r(()=>[e[1]||(e[1]=E("span",null,"快照·业务单创建时间",-1)),n(l,{placement:"top",content:"related_po_create_time:触发本次操作的处方业务订单 create_time;与原文助一致时表示医助创建订单时间"},{default:r(()=>[n(o,{class:"assign-log-col-hint"},{default:r(()=>[n(B(F))]),_:1})]),_:1})]),default:r(({row:i})=>[c(d(x(i)),1)]),_:1}),n(t,{label:"操作人",width:"110",prop:"operator_name"}),n(t,{label:"操作账号",width:"120",prop:"operator_account","show-overflow-tooltip":""}),n(t,{label:"IP",width:"130",prop:"ip","show-overflow-tooltip":""})]),_:1},8,["data"])),[[I,m.value]])])}}}),Y=q(z,[["__scopeId","data-v-f670e3e6"]]);export{Y as default};
|
||||
@@ -1 +0,0 @@
|
||||
.assign-log-col-hint[data-v-f670e3e6]{margin-left:4px;vertical-align:middle;color:var(--el-text-color-secondary);cursor:help}
|
||||
@@ -0,0 +1 @@
|
||||
import{N as C,O as T,U as $,r as D,d as k,M as E}from"./element-plus-DGP1XOrU.js";import{Y as L}from"./@element-plus/icons-vue-CbYn5zW9.js";import{af as P}from"./tcm-CEIBuAeK.js";import{f as A,w as B,ak as p,I as b,aP as F,G as h,aN as n,a as i,O as m,J as M}from"./@vue/runtime-core-C6bnekPw.js";import{Q as c}from"./@vue/shared-mAAVTE9n.js";import{y as V,n as w}from"./@vue/reactivity-DiY1c2vO.js";import{_ as K}from"./index-fRv3NTFQ.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const O={class:"assign-log-panel"},Y={key:1,class:"text-gray-400"},q=A({__name:"AssignLogPanel",props:{diagnosisId:{}},setup(N,{expose:v}){const _=N,d=w(!1),u=w([]);function y(o){const e=o.related_po_creator_name;if(e!=null&&String(e).trim()!=="")return String(e);const t=Number(o.related_po_creator_id);return Number.isFinite(t)&&t>0?`ID:${t}`:"—"}function x(o){const e=o.related_po_create_time_text;if(e!=null&&String(e).trim()!=="")return String(e);const t=Number(o.related_po_create_time);if(!Number.isFinite(t)||t<=0)return"—";const a=new Date(t*1e3);if(Number.isNaN(a.getTime()))return"—";const r=l=>String(l).padStart(2,"0");return`${a.getFullYear()}-${r(a.getMonth()+1)}-${r(a.getDate())} ${r(a.getHours())}:${r(a.getMinutes())}:${r(a.getSeconds())}`}function f(o,e){const t=e==="from"?"from_assistant_name":"to_assistant_name",a=e==="from"?"from_assistant_id":"to_assistant_id",r=o[t];if(r!=null&&String(r).trim()!==""&&String(r)!=="—")return String(r);const l=Number(o[a]);return Number.isFinite(l)&&l>0?`ID:${l}`:"—"}const g=async()=>{if(_.diagnosisId){d.value=!0;try{const o=await P({id:_.diagnosisId}),e=Array.isArray(o)?o:[];u.value=e}catch(o){console.error(o),u.value=[]}finally{d.value=!1}}};return B(()=>_.diagnosisId,()=>{g()},{immediate:!0}),v({refresh:g}),(o,e)=>{const t=T,a=$,r=k,l=D,S=C,I=E;return p(),b("div",O,[F((p(),h(S,{data:u.value,border:"",stripe:"","empty-text":"暂无指派记录"},{default:n(()=>[i(t,{label:"操作时间",width:"175",prop:"create_time_text"}),i(t,{label:"原医助","min-width":"120"},{default:n(({row:s})=>[m(c(f(s,"from")),1)]),_:1}),i(t,{label:"新医助","min-width":"120"},{default:n(({row:s})=>[m(c(f(s,"to")),1)]),_:1}),i(t,{label:"继承",width:"72",align:"center"},{default:n(({row:s})=>[Number(s.is_inherit)===1?(p(),h(a,{key:0,type:"success",size:"small"},{default:n(()=>[...e[0]||(e[0]=[m("是",-1)])]),_:1})):(p(),b("span",Y,"否"))]),_:1}),i(t,{label:"快照·业务单创建人","min-width":"130","show-overflow-tooltip":""},{default:n(({row:s})=>[m(c(y(s)),1)]),_:1}),i(t,{label:"快照·业务单创建时间",width:"190"},{header:n(()=>[e[1]||(e[1]=M("span",null,"快照·业务单创建时间",-1)),i(l,{placement:"top",content:"related_po_create_time:触发本次操作的处方业务订单 create_time;与原文助一致时表示医助创建订单时间"},{default:n(()=>[i(r,{class:"assign-log-col-hint"},{default:n(()=>[i(V(L))]),_:1})]),_:1})]),default:n(({row:s})=>[m(c(x(s)),1)]),_:1}),i(t,{label:"操作人",width:"110",prop:"operator_name"}),i(t,{label:"操作账号",width:"120",prop:"operator_account","show-overflow-tooltip":""}),i(t,{label:"IP",width:"130",prop:"ip","show-overflow-tooltip":""})]),_:1},8,["data"])),[[I,d.value]])])}}}),Tt=K(q,[["__scopeId","data-v-0a09e3d2"]]);export{Tt as default};
|
||||
@@ -1 +0,0 @@
|
||||
import{o as B,R as D,q as E,O,D as I,r as _,T as S,P as x,ac as W,s as h,ad as P,v as U,K as $,L as j,bt as H,p as K,M as v,di as c}from"./.pnpm-BSw4l71J.js";import{ag as Y}from"./tcm-DKwa3Njw.js";import{_ as q}from"./index-0obBWZiZ.js";const z={key:0,class:"watch-state"},F={key:1,class:"watch-state watch-error"},G=B({__name:"AssistantWatchCallDialog",props:{modelValue:{type:Boolean},diagnosisId:{}},emits:["update:modelValue","closed"],setup(R,{emit:A}){const u=R,g=A,y=K({get:()=>u.modelValue,set:t=>g("update:modelValue",t)}),r=v(null),l=v(!1),i=v(""),f=v("旁观视频通话"),n=new Map;let a=null,p=0;function w(t,e){return`${t}\0${String(e)}`}function N(t){return t.startsWith("patient_")?"患者":t.startsWith("doctor_")?"医护":t}async function T(t){if(!a||!r.value||t.streamType!==c.TYPE.STREAM_TYPE_MAIN)return;const e=w(t.userId,t.streamType);if(n.has(e))return;const o=document.createElement("div");o.className="watch-tile";const s=document.createElement("div");s.className="watch-tile-cap",s.textContent=N(t.userId);const d=document.createElement("div");d.className="watch-tile-view",o.appendChild(s),o.appendChild(d),r.value.appendChild(o),n.set(e,{wrap:o,userId:t.userId,streamType:t.streamType});try{await a.startRemoteVideo({userId:t.userId,streamType:t.streamType,view:d})}catch(b){console.warn("[AssistantWatchCall] startRemoteVideo",b)}}async function V(t){if(!a)return;const e=w(t.userId,t.streamType),o=n.get(e);if(o){try{await a.stopRemoteVideo({userId:t.userId,streamType:t.streamType})}catch{}o.wrap.remove(),n.delete(e)}}function C(){a&&(a.on(c.EVENT.REMOTE_VIDEO_AVAILABLE,T),a.on(c.EVENT.REMOTE_VIDEO_UNAVAILABLE,V))}function k(){a&&(a.off(c.EVENT.REMOTE_VIDEO_AVAILABLE,T),a.off(c.EVENT.REMOTE_VIDEO_UNAVAILABLE,V))}async function m(){if(k(),a){for(const[,t]of n){try{await a.stopRemoteVideo({userId:t.userId,streamType:t.streamType})}catch{}t.wrap.remove()}n.clear(),r.value&&(r.value.innerHTML="");try{await a.exitRoom()}catch{}try{a.destroy()}catch{}a=null}else n.clear(),r.value&&(r.value.innerHTML="")}async function L(){const t=++p;if(await m(),!u.diagnosisId){i.value="诊单无效";return}l.value=!0,i.value="",f.value="旁观视频通话";try{const e=await Y({diagnosis_id:u.diagnosisId});if(t!==p)return;e.patientName&&(f.value=`旁观视频通话 · ${e.patientName}`),a=c.create(),C();const o={sdkAppId:e.sdkAppId,userId:e.userId,userSig:e.userSig,autoReceiveAudio:!0,autoReceiveVideo:!0,...e.roomId!=null&&e.roomId>0?{roomId:e.roomId}:{strRoomId:e.strRoomId}};if(!(e.roomId!=null&&e.roomId>0)&&!e.strRoomId)throw new Error("缺少房间号");if(await a.enterRoom(o),t!==p){await m();return}l.value=!1}catch(e){l.value=!1;let o="进入房间失败";if(typeof e=="string")o=e;else if(e&&typeof e=="object"){const s=e;s.msg?o=String(s.msg):s.message&&(o=String(s.message))}i.value=o,await m()}}function M(){m(),l.value=!1,i.value="",f.value="旁观视频通话",g("closed")}return D(()=>[u.modelValue,u.diagnosisId],([t,e])=>{if(!t){p++,m();return}e>0&&L()}),(t,e)=>{const o=$,s=H;return E(),O(s,{modelValue:y.value,"onUpdate:modelValue":e[1]||(e[1]=d=>y.value=d),title:f.value,width:"760px","destroy-on-close":"","append-to-body":"","close-on-click-modal":!1,class:"assistant-watch-call-dialog",onClosed:M},{footer:I(()=>[e[3]||(e[3]=h("span",{class:"watch-hint"},"仅观看,不会开启摄像头与麦克风",-1)),U(o,{type:"primary",onClick:e[0]||(e[0]=d=>y.value=!1)},{default:I(()=>[...e[2]||(e[2]=[j("离开",-1)])]),_:1})]),default:I(()=>[l.value?(E(),_("div",z,"正在连接房间…")):i.value?(E(),_("div",F,S(i.value),1)):x("",!0),W(h("div",{ref_key:"gridRef",ref:r,class:"watch-grid"},null,512),[[P,!l.value&&!i.value]])]),_:1},8,["modelValue","title"])}}}),Z=q(G,[["__scopeId","data-v-7aff665d"]]);export{Z as default};
|
||||
@@ -1 +0,0 @@
|
||||
.watch-state[data-v-7aff665d]{min-height:200px;display:flex;align-items:center;justify-content:center;color:var(--el-text-color-secondary);font-size:14px}.watch-error[data-v-7aff665d]{color:var(--el-color-danger)}.watch-grid[data-v-7aff665d]{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px;min-height:220px}.watch-tile[data-v-7aff665d]{background:#0f0f0f;border-radius:8px;overflow:hidden;aspect-ratio:16 / 10;display:flex;flex-direction:column}.watch-tile-cap[data-v-7aff665d]{padding:6px 10px;font-size:12px;color:#e5e5e5;background:#0000008c}.watch-tile-view[data-v-7aff665d]{flex:1;min-height:0;position:relative}.watch-hint[data-v-7aff665d]{float:left;line-height:32px;font-size:12px;color:var(--el-text-color-secondary)}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
.watch-state[data-v-8c719418]{min-height:200px;display:flex;align-items:center;justify-content:center;color:var(--el-text-color-secondary);font-size:14px}.watch-error[data-v-8c719418]{color:var(--el-color-danger)}.watch-grid[data-v-8c719418]{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px;min-height:220px}.watch-tile[data-v-8c719418]{background:#0f0f0f;border-radius:8px;overflow:hidden;aspect-ratio:16 / 10;display:flex;flex-direction:column}.watch-tile-cap[data-v-8c719418]{padding:6px 10px;font-size:12px;color:#e5e5e5;background:#0000008c}.watch-tile-view[data-v-8c719418]{flex:1;min-height:0;position:relative}.watch-hint[data-v-8c719418]{float:left;line-height:32px;font-size:12px;color:var(--el-text-color-secondary)}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
import{o as N,dk as O,R as P,q as d,r as f,v as o,D as i,K as V,L as n,P as h,ac as D,O as w,bj as L,bk as z,T as u,s as y,bi as M,M as v}from"./.pnpm-BSw4l71J.js";import j from"./RecordingPlaybackBlock-2wN2LtoH.js";import{U as k}from"./index-BqGqHzpB.js";import{i as c,_ as q}from"./index-0obBWZiZ.js";import{ak as K,al as x,am as A}from"./tcm-DKwa3Njw.js";import"./RecordingVideoPlayer-CTZLdMyy.js";import"./file-DBDqscMe.js";const F={class:"call-record-panel"},G={key:0,class:"call-record-toolbar"},H={class:"call-record-empty"},J={class:"call-record-empty__desc"},Q={key:0,class:"text-primary"},W={key:1,class:"text-gray-400"},X=N({__name:"CallRecordPanel",props:{diagnosisId:{},readOnly:{type:Boolean,default:!1}},setup(_,{expose:R}){const r=_,p=v(!1),g=v([]),U=O("toolbarUploadRef"),m=async()=>{if(r.diagnosisId){p.value=!0;try{g.value=await K({diagnosis_id:r.diagnosisId})||[]}catch(e){console.error(e),g.value=[]}finally{p.value=!1}}};P(()=>r.diagnosisId,()=>{m()},{immediate:!0}),R({refresh:m});function C(e){return{1:"进行中",2:"已结束",3:"未接听",4:"已取消"}[e]??"—"}async function S(e){const t=b(e);if(!t){c.msgError("上传成功但未返回视频地址");return}try{const a=await A({diagnosis_id:r.diagnosisId});await x({diagnosis_id:r.diagnosisId,call_record_id:Number((a==null?void 0:a.id)||0),file_url:t}),c.msgSuccess("视频回放上传成功"),await m()}catch(a){c.msgError((a==null?void 0:a.message)||"写入回放失败")}}async function E(e,t){const a=b(t);if(!a){c.msgError("上传成功但未返回视频地址");return}try{await x({diagnosis_id:r.diagnosisId,call_record_id:Number(e.id||0),file_url:a}),c.msgSuccess("视频回放上传成功"),await m()}catch(l){c.msgError((l==null?void 0:l.message)||"写入回放失败")}}function b(e){var t,a;return String(((t=e==null?void 0:e.data)==null?void 0:t.uri)||((a=e==null?void 0:e.data)==null?void 0:a.url)||"").trim()}return(e,t)=>{const a=V,l=z,I=L,B=M;return d(),f("div",F,[_.readOnly?h("",!0):(d(),f("div",G,[o(k,{ref_key:"toolbarUploadRef",ref:U,type:"video",direct:"",multiple:!1,limit:1,"show-progress":!0,onSuccess:S},{default:i(()=>[o(a,{type:"primary"},{default:i(()=>[...t[0]||(t[0]=[n("上传视频",-1)])]),_:1})]),_:1},512)])),D((d(),w(I,{data:g.value,border:"",stripe:""},{empty:i(()=>[y("div",H,[t[1]||(t[1]=y("div",{class:"call-record-empty__title"},"暂无通话记录",-1)),y("div",J,u(_.readOnly?"暂无录制回放数据。":"现在可以直接点击上方“上传视频”。系统会自动生成一条默认通话记录来承载回放。"),1)])]),default:i(()=>[o(l,{label:"录制回放","min-width":"320"},{default:i(({row:s})=>[o(j,{"record-id":s.id,urls:s.recording_urls_list},null,8,["record-id","urls"])]),_:1}),o(l,{label:"开始时间",width:"170",prop:"start_time_text"}),o(l,{label:"结束时间",width:"170",prop:"end_time_text"}),o(l,{label:"通话类型",width:"100"},{default:i(({row:s})=>[n(u(s.call_type===1?"语音":"视频"),1)]),_:1}),o(l,{label:"房间号",width:"180"},{default:i(({row:s})=>[s.room_id?(d(),f("span",Q,u(s.room_id),1)):(d(),f("span",W,"—"))]),_:1}),o(l,{label:"时长",width:"110",prop:"duration_text"}),o(l,{label:"状态",width:"90"},{default:i(({row:s})=>[n(u(C(s.status)),1)]),_:1}),o(l,{label:"录制",width:"100"},{default:i(({row:s})=>[n(u(s.recording_status_text||"—"),1)]),_:1}),_.readOnly?h("",!0):(d(),w(l,{key:0,label:"上传回放",width:"180"},{default:i(({row:s})=>[o(k,{type:"video",direct:"",multiple:!1,limit:1,"show-progress":!0,onSuccess:T=>E(s,T)},{default:i(()=>[o(a,{type:"primary",plain:"",size:"small"},{default:i(()=>[...t[2]||(t[2]=[n("上传视频",-1)])]),_:1})]),_:1},8,["onSuccess"])]),_:1}))]),_:1},8,["data"])),[[B,p.value]])])}}}),sa=q(X,[["__scopeId","data-v-78d5c9e4"]]);export{sa as default};
|
||||
@@ -0,0 +1 @@
|
||||
import{i as V,M as D,O as L,U as O,N as T,X as z}from"./element-plus-DGP1XOrU.js";import{an as M}from"./tcm-CEIBuAeK.js";import{f as P,b as F,w as j,ak as a,I as n,a as i,aN as s,O as m,H as w,aP as A,G as g,F as H,J as R}from"./@vue/runtime-core-C6bnekPw.js";import{y as d,n as k}from"./@vue/reactivity-DiY1c2vO.js";import{Q as c}from"./@vue/shared-mAAVTE9n.js";import{_ as G}from"./index-fRv3NTFQ.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@element-plus/icons-vue-CbYn5zW9.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const J={class:"case-record-list"},Q={key:0,class:"mb-3 flex justify-end"},U={key:0},X={key:1,class:"text-gray-400"},Y={class:"void-detail text-xs text-gray-500 mt-1"},q=P({__name:"CaseRecordList",props:{diagnosisId:{type:Number,default:0},readOnly:{type:Boolean,default:!1}},emits:["view","openPrescription"],setup(y,{expose:x,emit:C}){const _=y,h=C,l=k([]),p=k(!1),u=async()=>{if(_.diagnosisId){p.value=!0;try{const e=await M({diagnosis_id:_.diagnosisId});l.value=Array.isArray(e)?e:[]}catch(e){console.error("获取病历记录失败:",e),l.value=[]}finally{p.value=!1}}},S=e=>{if(!e)return"";const t=new Date(e*1e3);return`${t.getFullYear()}-${String(t.getMonth()+1).padStart(2,"0")}-${String(t.getDate()).padStart(2,"0")} ${String(t.getHours()).padStart(2,"0")}:${String(t.getMinutes()).padStart(2,"0")}`},$=e=>{h("view",e)},B=()=>{h("openPrescription")};return F(()=>{u()}),j(()=>_.diagnosisId,()=>{u()}),x({refresh:u}),(e,t)=>{const b=V,r=L,v=O,E=T,N=z,I=D;return a(),n("div",J,[y.readOnly?w("",!0):(a(),n("div",Q,[i(b,{type:"primary",size:"small",onClick:B},{default:s(()=>[...t[0]||(t[0]=[m("开方",-1)])]),_:1})])),A((a(),g(E,{data:d(l),border:""},{default:s(()=>[i(r,{prop:"prescription_date",label:"就诊日期",width:"120"}),i(r,{prop:"visit_no",label:"门诊号",width:"120"}),i(r,{prop:"clinical_diagnosis",label:"临床诊断","min-width":"160","show-overflow-tooltip":""}),i(r,{label:"处方摘要","min-width":"180"},{default:s(({row:o})=>[o.herbs&&o.herbs.length?(a(),n("span",U,c(o.herbs.slice(0,3).map(f=>`${f.name}${f.dosage}克`).join("、"))+c(o.herbs.length>3?"...":""),1)):(a(),n("span",X,"—"))]),_:1}),i(r,{prop:"doctor_name",label:"医师",width:"90","show-overflow-tooltip":""}),i(r,{label:"状态",width:"140",align:"center"},{default:s(({row:o})=>[o.void_status===1?(a(),n(H,{key:0},[i(v,{type:"danger",size:"small"},{default:s(()=>[...t[1]||(t[1]=[m("已作废",-1)])]),_:1}),R("div",Y,c(o.void_by_name||"—")+" "+c(S(o.void_time)),1)],64)):(a(),g(v,{key:1,type:"success",size:"small"},{default:s(()=>[...t[2]||(t[2]=[m("正常",-1)])]),_:1}))]),_:1}),i(r,{label:"操作",width:"120",fixed:"right"},{default:s(({row:o})=>[i(b,{link:"",type:"primary",size:"small",onClick:f=>$(o)},{default:s(()=>[...t[3]||(t[3]=[m(" 查看 ",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["data"])),[[I,d(p)]]),!d(p)&&d(l).length===0?(a(),g(N,{key:1,description:"暂无病历记录,开方后会自动显示",class:"mt-4"})):w("",!0)])}}}),Tt=G(q,[["__scopeId","data-v-043d2738"]]);export{Tt as default};
|
||||
@@ -1 +0,0 @@
|
||||
import{o as T,ap as V,R as I,q as o,r as l,v as a,K as N,D as i,L as c,P as k,ac as z,bi as M,u as p,O as f,bk as O,T as _,F as P,br as j,s as F,bj as R,bB as A,M as w}from"./.pnpm-BSw4l71J.js";import{an as q}from"./tcm-DKwa3Njw.js";import{_ as H}from"./index-0obBWZiZ.js";const K={class:"case-record-list"},Y={key:0,class:"mb-3 flex justify-end"},G={key:0},J={key:1,class:"text-gray-400"},Q={class:"void-detail text-xs text-gray-500 mt-1"},U=T({__name:"CaseRecordList",props:{diagnosisId:{type:Number,default:0},readOnly:{type:Boolean,default:!1}},emits:["view","openPrescription"],setup(y,{expose:x,emit:C}){const m=y,b=C,r=w([]),d=w(!1),u=async()=>{if(m.diagnosisId){d.value=!0;try{const t=await q({diagnosis_id:m.diagnosisId});r.value=Array.isArray(t)?t:[]}catch(t){console.error("获取病历记录失败:",t),r.value=[]}finally{d.value=!1}}},S=t=>{if(!t)return"";const e=new Date(t*1e3);return`${e.getFullYear()}-${String(e.getMonth()+1).padStart(2,"0")}-${String(e.getDate()).padStart(2,"0")} ${String(e.getHours()).padStart(2,"0")}:${String(e.getMinutes()).padStart(2,"0")}`},B=t=>{b("view",t)},$=()=>{b("openPrescription")};return V(()=>{u()}),I(()=>m.diagnosisId,()=>{u()}),x({refresh:u}),(t,e)=>{const h=N,n=O,v=j,E=R,D=A,L=M;return o(),l("div",K,[y.readOnly?k("",!0):(o(),l("div",Y,[a(h,{type:"primary",size:"small",onClick:$},{default:i(()=>[...e[0]||(e[0]=[c("开方",-1)])]),_:1})])),z((o(),f(E,{data:p(r),border:""},{default:i(()=>[a(n,{prop:"prescription_date",label:"就诊日期",width:"120"}),a(n,{prop:"visit_no",label:"门诊号",width:"120"}),a(n,{prop:"clinical_diagnosis",label:"临床诊断","min-width":"160","show-overflow-tooltip":""}),a(n,{label:"处方摘要","min-width":"180"},{default:i(({row:s})=>[s.herbs&&s.herbs.length?(o(),l("span",G,_(s.herbs.slice(0,3).map(g=>`${g.name}${g.dosage}克`).join("、"))+_(s.herbs.length>3?"...":""),1)):(o(),l("span",J,"—"))]),_:1}),a(n,{prop:"doctor_name",label:"医师",width:"90","show-overflow-tooltip":""}),a(n,{label:"状态",width:"140",align:"center"},{default:i(({row:s})=>[s.void_status===1?(o(),l(P,{key:0},[a(v,{type:"danger",size:"small"},{default:i(()=>[...e[1]||(e[1]=[c("已作废",-1)])]),_:1}),F("div",Q,_(s.void_by_name||"—")+" "+_(S(s.void_time)),1)],64)):(o(),f(v,{key:1,type:"success",size:"small"},{default:i(()=>[...e[2]||(e[2]=[c("正常",-1)])]),_:1}))]),_:1}),a(n,{label:"操作",width:"120",fixed:"right"},{default:i(({row:s})=>[a(h,{link:"",type:"primary",size:"small",onClick:g=>B(s)},{default:i(()=>[...e[3]||(e[3]=[c(" 查看 ",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["data"])),[[L,p(d)]]),!p(d)&&p(r).length===0?(o(),f(D,{key:1,description:"暂无病历记录,开方后会自动显示",class:"mt-4"})):k("",!0)])}}}),ee=H(U,[["__scopeId","data-v-043d2738"]]);export{ee as default};
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
.daily-matrix[data-v-4d566204]{padding:16px}.daily-matrix__toolbar[data-v-4d566204]{display:flex;justify-content:space-between;gap:12px;align-items:center;flex-wrap:wrap;margin-bottom:12px}.daily-matrix__toolbar-left[data-v-4d566204],.daily-matrix__toolbar-right[data-v-4d566204]{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.daily-matrix__table[data-v-4d566204],.daily-matrix__table-wrap[data-v-4d566204]{width:100%}.daily-matrix__chart[data-v-4d566204]{margin-top:16px;padding:16px 18px;border:1px solid var(--el-border-color-lighter);border-radius:10px;background:linear-gradient(180deg,#fff,#f8fafc)}.daily-matrix__chart-canvas[data-v-4d566204]{height:280px;width:100%}.daily-matrix__cell[data-v-4d566204]{min-height:32px;display:inline-flex;align-items:center;justify-content:center;gap:2px;width:100%;color:var(--el-text-color-regular)}.daily-matrix__cell.is-clickable[data-v-4d566204]{cursor:pointer}.daily-matrix__cell.is-empty[data-v-4d566204]{color:var(--el-text-color-placeholder)}.daily-matrix__cell.is-high[data-v-4d566204]{color:#dc2626;font-weight:700}.daily-matrix__cell.is-patient-self[data-v-4d566204]{position:relative;background:linear-gradient(180deg,#8b5cf60a,#8b5cf61a);border-radius:4px}.daily-matrix__cell-up[data-v-4d566204]{color:#dc2626;font-size:13px}.daily-matrix__cell-patient[data-v-4d566204]{display:inline-block;margin-left:4px;padding:1px 6px;font-size:11px;font-weight:600;color:#6d28d9;background:#ede9fe;border:1px solid #ddd6fe;border-radius:999px;line-height:1.2;letter-spacing:.5px;white-space:nowrap}.daily-matrix__legend[data-v-4d566204]{display:inline-flex;align-items:center;gap:6px;margin-right:12px;padding:2px 10px 2px 6px;background:#f8f6ff;border:1px dashed #ddd6fe;border-radius:999px}.daily-matrix__legend-text[data-v-4d566204]{font-size:12px;color:#6d28d9;font-weight:500}.daily-matrix__todo[data-v-4d566204]{margin-top:16px}.daily-matrix__section-title[data-v-4d566204]{font-size:14px;font-weight:600;margin-bottom:12px;color:var(--el-text-color-primary)}.daily-matrix__tracking-existing[data-v-4d566204]{width:100%;max-height:180px;overflow:auto;padding:8px 10px;border:1px solid var(--el-border-color);border-radius:6px;background:var(--el-fill-color-light)}.daily-matrix__tracking-line[data-v-4d566204]{font-size:12.5px;line-height:1.6;color:var(--el-text-color-regular);word-break:break-word}.daily-matrix__tracking-preview[data-v-4d566204]{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px;width:100%;text-align:left}.daily-matrix__tracking-tooltip[data-v-4d566204]{max-width:320px}.daily-matrix__tracking-tooltip-line[data-v-4d566204]{font-size:12.5px;line-height:1.6;word-break:break-word}@media (max-width: 768px){.daily-matrix[data-v-4d566204],.daily-matrix__chart[data-v-4d566204]{padding:12px}.daily-matrix__chart-canvas[data-v-4d566204]{height:240px}}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
.daily-matrix[data-v-a5368e74]{padding:16px}.daily-matrix__toolbar[data-v-a5368e74]{display:flex;justify-content:space-between;gap:12px;align-items:center;flex-wrap:wrap;margin-bottom:12px}.daily-matrix__toolbar-left[data-v-a5368e74],.daily-matrix__toolbar-right[data-v-a5368e74]{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.daily-matrix__table[data-v-a5368e74],.daily-matrix__table-wrap[data-v-a5368e74]{width:100%}.daily-matrix__chart[data-v-a5368e74]{margin-top:16px;padding:16px 18px;border:1px solid var(--el-border-color-lighter);border-radius:10px;background:linear-gradient(180deg,#fff,#f8fafc)}.daily-matrix__chart-canvas[data-v-a5368e74]{height:280px;width:100%}.daily-matrix__cell[data-v-a5368e74]{min-height:32px;display:inline-flex;align-items:center;justify-content:center;gap:2px;width:100%;color:var(--el-text-color-regular)}.daily-matrix__cell.is-clickable[data-v-a5368e74]{cursor:pointer}.daily-matrix__cell.is-empty[data-v-a5368e74]{color:var(--el-text-color-placeholder)}.daily-matrix__cell.is-high[data-v-a5368e74]{color:#dc2626;font-weight:700}.daily-matrix__cell.is-patient-self[data-v-a5368e74]{position:relative;background:linear-gradient(180deg,#8b5cf60a,#8b5cf61a);border-radius:4px}.daily-matrix__cell-up[data-v-a5368e74]{color:#dc2626;font-size:13px}.daily-matrix__cell-patient[data-v-a5368e74]{display:inline-block;margin-left:4px;padding:1px 6px;font-size:11px;font-weight:600;color:#6d28d9;background:#ede9fe;border:1px solid #ddd6fe;border-radius:999px;line-height:1.2;letter-spacing:.5px;white-space:nowrap}.daily-matrix__legend[data-v-a5368e74]{display:inline-flex;align-items:center;gap:6px;margin-right:12px;padding:2px 10px 2px 6px;background:#f8f6ff;border:1px dashed #ddd6fe;border-radius:999px}.daily-matrix__legend-text[data-v-a5368e74]{font-size:12px;color:#6d28d9;font-weight:500}.daily-matrix__todo[data-v-a5368e74]{margin-top:16px}.daily-matrix__section-title[data-v-a5368e74]{font-size:14px;font-weight:600;margin-bottom:12px;color:var(--el-text-color-primary)}.daily-matrix__tracking-existing[data-v-a5368e74]{width:100%;max-height:180px;overflow:auto;padding:8px 10px;border:1px solid var(--el-border-color);border-radius:6px;background:var(--el-fill-color-light)}.daily-matrix__tracking-line[data-v-a5368e74]{font-size:12.5px;line-height:1.6;color:var(--el-text-color-regular);word-break:break-word}.daily-matrix__tracking-preview[data-v-a5368e74]{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px;width:100%;text-align:left}.daily-matrix__tracking-tooltip[data-v-a5368e74]{max-width:320px}.daily-matrix__tracking-tooltip-line[data-v-a5368e74]{font-size:12.5px;line-height:1.6;word-break:break-word}@media(max-width:768px){.daily-matrix[data-v-a5368e74],.daily-matrix__chart[data-v-a5368e74]{padding:12px}.daily-matrix__chart-canvas[data-v-a5368e74]{height:240px}}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
.diagnosis-todo-list .toolbar[data-v-c136d72f]{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-bottom:12px}.diagnosis-todo-list .todo-table[data-v-c136d72f]{width:100%}.diagnosis-todo-list .pagination-wrap[data-v-c136d72f]{margin-top:12px;display:flex;justify-content:flex-end}.diagnosis-todo-list .text-danger[data-v-c136d72f]{color:var(--el-color-danger)}.diagnosis-todo-list .text-muted[data-v-c136d72f]{color:var(--el-text-color-placeholder)}
|
||||
@@ -0,0 +1 @@
|
||||
.diagnosis-todo-list .toolbar[data-v-e71c3261]{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-bottom:12px}.diagnosis-todo-list .todo-table[data-v-e71c3261]{width:100%}.diagnosis-todo-list .pagination-wrap[data-v-e71c3261]{margin-top:12px;display:flex;justify-content:flex-end}.diagnosis-todo-list .text-danger[data-v-e71c3261]{color:var(--el-color-danger)}.diagnosis-todo-list .text-muted[data-v-e71c3261]{color:var(--el-text-color-placeholder)}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
import{_ as o}from"./GancaoSubmissionReconcileButton.vue_vue_type_script_setup_true_lang-BDl0pktY.js";import"./.pnpm-BSw4l71J.js";import"./tcm-DKwa3Njw.js";import"./index-0obBWZiZ.js";export{o as default};
|
||||
@@ -0,0 +1 @@
|
||||
import{_ as o}from"./GancaoSubmissionReconcileButton.vue_vue_type_script_setup_true_lang-DPPpLv5I.js";import"./element-plus-DGP1XOrU.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/runtime-core-C6bnekPw.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@vue/shared-mAAVTE9n.js";import"./@element-plus/icons-vue-CbYn5zW9.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./tcm-CEIBuAeK.js";import"./index-fRv3NTFQ.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";export{o as default};
|
||||
-1
@@ -1 +0,0 @@
|
||||
import{o as U,be as F,q as f,r as k,F as M,ac as D,O as E,D as t,L as u,K as G,v as r,bt as B,bh as q,b9 as P,aw as A,b6 as L,bc as T,bf as K,b7 as z,P as g,p as W,M as v,ba as $}from"./.pnpm-BSw4l71J.js";import{p as j}from"./tcm-DKwa3Njw.js";import{i as C}from"./index-0obBWZiZ.js";const X=U({__name:"GancaoSubmissionReconcileButton",props:{order:{}},emits:["resolved"],setup(N,{emit:w}){const d=N,y=w,V=W(()=>{var m,p,a;const i=String(((m=d.order)==null?void 0:m.pharmacy_claim_target)||"").trim().toLowerCase(),e=String(((p=d.order)==null?void 0:p.pharmacy_claim_status)||"").trim().toUpperCase(),n=Number(((a=d.order)==null?void 0:a.pharmacy_claim_lease_expires_at)||0),c=e==="PENDING"&&n>0&&n<=Math.floor(Date.now()/1e3);return i==="gancao"&&(c||["UNKNOWN","PENDING_RECONCILE"].includes(e))}),s=v(!1),_=v(!1),o=$({resolution:"CONFIRM_SUCCESS",remote_order_no:"",note:""});function O(){o.resolution="CONFIRM_SUCCESS",o.remote_order_no="",o.note="",s.value=!0}async function b(){var e;const i=Number((e=d.order)==null?void 0:e.id);if(i){if(o.resolution==="CONFIRM_SUCCESS"&&!o.remote_order_no.trim()){C.msgError("请填写甘草药方单号");return}if(!o.note.trim()){C.msgError("请填写甘草后台核对依据");return}_.value=!0;try{await j({id:i,resolution:o.resolution,remote_order_no:o.resolution==="CONFIRM_SUCCESS"?o.remote_order_no.trim():"",note:o.note.trim()}),C.msgSuccess("甘草提交核对已记录"),s.value=!1,y("resolved")}catch{}finally{_.value=!1}}}return(i,e)=>{const n=G,c=q,m=K,p=T,a=L,S=z,R=P,x=B,I=F("perms");return V.value?(f(),k(M,{key:0},[D((f(),E(n,{type:"danger",size:"small",plain:"",onClick:O},{default:t(()=>[...e[5]||(e[5]=[u("核对甘草提交",-1)])]),_:1})),[[I,["tcm.prescriptionOrder/confirmGancaoSubmission"]]]),r(x,{modelValue:s.value,"onUpdate:modelValue":e[4]||(e[4]=l=>s.value=l),title:"人工核对甘草提交",width:"min(92vw, 520px)","append-to-body":"","destroy-on-close":"","close-on-click-modal":!1},{footer:t(()=>[r(n,{onClick:e[3]||(e[3]=l=>s.value=!1)},{default:t(()=>[...e[8]||(e[8]=[u("取消",-1)])]),_:1}),r(n,{type:"primary",loading:_.value,onClick:b},{default:t(()=>[...e[9]||(e[9]=[u("确认并记录",-1)])]),_:1},8,["loading"])]),default:t(()=>[r(c,{title:"请先在甘草后台核对。本操作会写入不可变更的审计记录。",type:"warning",closable:!1,"show-icon":"",class:"mb-4"}),r(R,{"label-width":"100px",onSubmit:A(b,["prevent"])},{default:t(()=>[r(a,{label:"核对结果",required:""},{default:t(()=>[r(p,{modelValue:o.resolution,"onUpdate:modelValue":e[0]||(e[0]=l=>o.resolution=l)},{default:t(()=>[r(m,{label:"CONFIRM_SUCCESS"},{default:t(()=>[...e[6]||(e[6]=[u("确认已创建",-1)])]),_:1}),r(m,{label:"CONFIRM_NOT_CREATED"},{default:t(()=>[...e[7]||(e[7]=[u("确认未创建",-1)])]),_:1})]),_:1},8,["modelValue"])]),_:1}),o.resolution==="CONFIRM_SUCCESS"?(f(),E(a,{key:0,label:"甘草单号",required:""},{default:t(()=>[r(S,{modelValue:o.remote_order_no,"onUpdate:modelValue":e[1]||(e[1]=l=>o.remote_order_no=l),maxlength:"64",clearable:""},null,8,["modelValue"])]),_:1})):g("",!0),r(a,{label:"核对依据",required:""},{default:t(()=>[r(S,{modelValue:o.note,"onUpdate:modelValue":e[2]||(e[2]=l=>o.note=l),type:"textarea",rows:3,maxlength:"1000","show-word-limit":"",placeholder:"例如:核对时间、甘草后台查询条件及结果"},null,8,["modelValue"])]),_:1})]),_:1})]),_:1},8,["modelValue"])],64)):g("",!0)}}});export{X as _};
|
||||
+1
@@ -0,0 +1 @@
|
||||
import{i as x,W as U,L as F,D,B as M,G,J as B,C as A}from"./element-plus-DGP1XOrU.js";import{s as P}from"./@vue/runtime-dom-DDAG46FW.js";import{p as q}from"./tcm-CEIBuAeK.js";import{k as f}from"./index-fRv3NTFQ.js";import{f as L,as as T,ak as C,I as W,F as z,aP as H,G as N,aN as t,O as u,a as r,H as b,A as J}from"./@vue/runtime-core-C6bnekPw.js";import{n as g,r as K}from"./@vue/reactivity-DiY1c2vO.js";const h=L({__name:"GancaoSubmissionReconcileButton",props:{order:{}},emits:["resolved"],setup(v,{emit:y}){const d=v,V=y,w=J(()=>{var m,p,a;const i=String(((m=d.order)==null?void 0:m.pharmacy_claim_target)||"").trim().toLowerCase(),e=String(((p=d.order)==null?void 0:p.pharmacy_claim_status)||"").trim().toUpperCase(),n=Number(((a=d.order)==null?void 0:a.pharmacy_claim_lease_expires_at)||0),c=e==="PENDING"&&n>0&&n<=Math.floor(Date.now()/1e3);return i==="gancao"&&(c||["UNKNOWN","PENDING_RECONCILE"].includes(e))}),s=g(!1),_=g(!1),o=K({resolution:"CONFIRM_SUCCESS",remote_order_no:"",note:""});function I(){o.resolution="CONFIRM_SUCCESS",o.remote_order_no="",o.note="",s.value=!0}async function S(){var e;const i=Number((e=d.order)==null?void 0:e.id);if(i){if(o.resolution==="CONFIRM_SUCCESS"&&!o.remote_order_no.trim()){f.msgError("请填写甘草药方单号");return}if(!o.note.trim()){f.msgError("请填写甘草后台核对依据");return}_.value=!0;try{await q({id:i,resolution:o.resolution,remote_order_no:o.resolution==="CONFIRM_SUCCESS"?o.remote_order_no.trim():"",note:o.note.trim()}),f.msgSuccess("甘草提交核对已记录"),s.value=!1,V("resolved")}catch{}finally{_.value=!1}}}return(i,e)=>{const n=x,c=F,m=B,p=G,a=M,E=A,O=D,R=U,k=T("perms");return w.value?(C(),W(z,{key:0},[H((C(),N(n,{type:"danger",size:"small",plain:"",onClick:I},{default:t(()=>[...e[5]||(e[5]=[u("核对甘草提交",-1)])]),_:1})),[[k,["tcm.prescriptionOrder/confirmGancaoSubmission"]]]),r(R,{modelValue:s.value,"onUpdate:modelValue":e[4]||(e[4]=l=>s.value=l),title:"人工核对甘草提交",width:"min(92vw, 520px)","append-to-body":"","destroy-on-close":"","close-on-click-modal":!1},{footer:t(()=>[r(n,{onClick:e[3]||(e[3]=l=>s.value=!1)},{default:t(()=>[...e[8]||(e[8]=[u("取消",-1)])]),_:1}),r(n,{type:"primary",loading:_.value,onClick:S},{default:t(()=>[...e[9]||(e[9]=[u("确认并记录",-1)])]),_:1},8,["loading"])]),default:t(()=>[r(c,{title:"请先在甘草后台核对。本操作会写入不可变更的审计记录。",type:"warning",closable:!1,"show-icon":"",class:"mb-4"}),r(O,{"label-width":"100px",onSubmit:P(S,["prevent"])},{default:t(()=>[r(a,{label:"核对结果",required:""},{default:t(()=>[r(p,{modelValue:o.resolution,"onUpdate:modelValue":e[0]||(e[0]=l=>o.resolution=l)},{default:t(()=>[r(m,{label:"CONFIRM_SUCCESS"},{default:t(()=>[...e[6]||(e[6]=[u("确认已创建",-1)])]),_:1}),r(m,{label:"CONFIRM_NOT_CREATED"},{default:t(()=>[...e[7]||(e[7]=[u("确认未创建",-1)])]),_:1})]),_:1},8,["modelValue"])]),_:1}),o.resolution==="CONFIRM_SUCCESS"?(C(),N(a,{key:0,label:"甘草单号",required:""},{default:t(()=>[r(E,{modelValue:o.remote_order_no,"onUpdate:modelValue":e[1]||(e[1]=l=>o.remote_order_no=l),maxlength:"64",clearable:""},null,8,["modelValue"])]),_:1})):b("",!0),r(a,{label:"核对依据",required:""},{default:t(()=>[r(E,{modelValue:o.note,"onUpdate:modelValue":e[2]||(e[2]=l=>o.note=l),type:"textarea",rows:3,maxlength:"1000","show-word-limit":"",placeholder:"例如:核对时间、甘草后台查询条件及结果"},null,8,["modelValue"])]),_:1})]),_:1})]),_:1},8,["modelValue"])],64)):b("",!0)}}});export{h as _};
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
import{o as F,R as q,q as t,r as n,v as i,D as c,s as o,L as _,bh as G,w as H,u as x,cX as $,K as j,by as J,ac as K,F as E,G as O,O as f,bB as W,P as v,bi as X,M as m,p as Q,ae as U,T as r,br as Z,aa as ee,bq as se,a8 as ae,E as C}from"./.pnpm-BSw4l71J.js";import{d as te}from"./dayjs-CVa8MSSA.js";import{as as ne,at as oe}from"./tcm-DKwa3Njw.js";import{p as re}from"./im-business-message-parse-oYIP1khU.js";import{_ as le}from"./index-0obBWZiZ.js";const ie={class:"im-chat-record-panel"},ce={class:"toolbar mb-3"},de={class:"chat-wrap"},_e={key:0,class:"chat-list"},ue={class:"meta"},fe={class:"name"},me={class:"time"},pe={class:"bubble"},ye={key:0,class:"friendly-text"},ge={class:"friendly-main"},ve={key:0,class:"friendly-sub"},he={key:1,class:"text-content"},we={key:2,class:"text-content"},be={key:3,class:"muted"},ke=F({__name:"ImChatRecordPanel",props:{diagnosisId:{}},setup(B,{expose:M}){const u=B,d=m(!1),p=m(!1),y=ae([]),L=m(""),g=m(""),D={text:"",image:"图片",file:"文件",sound:"语音",video:"视频",location:"位置",custom:"自定义",face:"表情",other:""},h=Q(()=>y.value.map(e=>{const a=P(e);let l="";return a!=null&&a.tag?l=a.tag:l=D[e.msg_type]||(e.msg_type!=="other"?e.msg_type:""),{raw:e,friendly:a,tag:l}}));function N(e){if(e==null||!e)return"—";const a=e>1e12?Math.floor(e/1e3):e;return te.unix(a).format("YYYY-MM-DD HH:mm:ss")}function P(e){const a=(e.text||"").trim();if(!a)return null;const l=a.startsWith("{")&&(/\bbusinessID\b/.test(a)||/\bcmd\b/.test(a));return e.msg_type==="custom"||l?re(a):null}function R(e){return e.is_from_doctor?e.from_staff_name?`医生(${e.from_staff_name})`:"医生/员工":g.value?`患者(${g.value})`:"患者"}async function w(){if(u.diagnosisId){d.value=!0;try{const e=await ne({diagnosis_id:u.diagnosisId,only_archived:1});y.value=(e==null?void 0:e.lists)||[],L.value=(e==null?void 0:e.patient_im_id)||"",g.value=(e==null?void 0:e.patient_name)||""}catch(e){console.error(e),y.value=[]}finally{d.value=!1}}}function b(){w()}async function S(){if(u.diagnosisId){p.value=!0;try{await oe({diagnosis_id:u.diagnosisId}),C.success('已发起后台同步,几秒后请点击"重新加载已归档"查看新消息')}catch(e){console.error(e),C.error("发起同步失败")}finally{p.value=!1}}}return q(()=>u.diagnosisId,()=>{w()},{immediate:!0}),M({refresh:b}),(e,a)=>{const l=G,k=H,I=j,T=Z,Y=ee,V=se,z=W,A=X;return t(),n("div",ie,[i(l,{type:"info","show-icon":"",closable:!1,class:"mb-4",title:"说明"},{default:c(()=>[...a[0]||(a[0]=[o("p",{class:"panel-tip"},[_(" 展示单聊记录:已合并患者 与 "),o("strong",null,"所有医生 / 医助账号"),_("分别产生的会话,按时间排序。 数据由后台定时任务从腾讯云 IM 漫游消息同步至本地归档;点击下方按钮可立即触发后台同步。 ")],-1)])]),_:1}),o("div",ce,[i(I,{type:"primary",link:"",loading:p.value,onClick:S},{default:c(()=>[i(k,{class:"mr-1"},{default:c(()=>[i(x($))]),_:1}),a[1]||(a[1]=_(" 同步最新(后台异步) ",-1))]),_:1},8,["loading"]),i(I,{type:"primary",link:"",loading:d.value,onClick:b},{default:c(()=>[i(k,{class:"mr-1"},{default:c(()=>[i(x(J))]),_:1}),a[2]||(a[2]=_(" 重新加载已归档 ",-1))]),_:1},8,["loading"])]),K((t(),n("div",de,[!d.value&&h.value.length?(t(),n("div",_e,[(t(!0),n(E,null,O(h.value,s=>(t(),n("div",{key:s.raw.msg_id,class:U(["chat-row",s.raw.is_from_doctor?"from-doctor":"from-patient"])},[o("div",ue,[o("span",fe,r(R(s.raw)),1),o("span",me,r(N(s.raw.time)),1),s.tag?(t(),f(T,{key:0,size:"small",type:"info",class:"ml-2"},{default:c(()=>[_(r(s.tag),1)]),_:2},1024)):v("",!0)]),o("div",pe,[s.raw.msg_type==="image"&&s.raw.image_url?(t(),f(Y,{key:0,src:s.raw.image_url,"preview-src-list":[s.raw.image_url],fit:"contain",class:"chat-img"},null,8,["src","preview-src-list"])):(s.raw.msg_type==="file"||s.raw.msg_type==="sound"||s.raw.msg_type==="video")&&s.raw.file_url?(t(),f(V,{key:1,href:s.raw.file_url,target:"_blank",type:"primary"},{default:c(()=>[_(r(s.raw.file_name||"打开文件"),1)]),_:2},1032,["href"])):(t(),n(E,{key:2},[s.friendly?(t(),n("div",ye,[o("div",ge,r(s.friendly.main),1),s.friendly.sub?(t(),n("div",ve,r(s.friendly.sub),1)):v("",!0)])):s.raw.msg_type==="text"&&s.raw.text?(t(),n("div",he,r(s.raw.text),1)):s.raw.text?(t(),n("div",we,r(s.raw.text),1)):(t(),n("span",be,"(无法展示该消息类型)"))],64))])],2))),128))])):d.value?v("",!0):(t(),f(z,{key:1,description:"暂无 IM 聊天记录"}))])),[[A,d.value]])])}}}),Me=le(ke,[["__scopeId","data-v-58b699dd"]]);export{Me as default};
|
||||
@@ -1 +0,0 @@
|
||||
import{_ as m}from"./MediaSourceSelect.vue_vue_type_script_setup_true_lang-CsbRGLkf.js";import"./.pnpm-BSw4l71J.js";export{m as default};
|
||||
@@ -0,0 +1 @@
|
||||
import{_ as o}from"./MediaSourceSelect.vue_vue_type_script_setup_true_lang-B0whCOSi.js";import"./element-plus-DGP1XOrU.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/runtime-core-C6bnekPw.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@vue/shared-mAAVTE9n.js";import"./@element-plus/icons-vue-CbYn5zW9.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";export{o as default};
|
||||
+1
@@ -0,0 +1 @@
|
||||
import{R as g,Q as V}from"./element-plus-DGP1XOrU.js";import{f as C,ak as o,G as s,aN as v,I as p,F as h,ap as B,H as k,A as m}from"./@vue/runtime-core-C6bnekPw.js";import{y as c}from"./@vue/reactivity-DiY1c2vO.js";import{p as w,o as S}from"./@vue/shared-mAAVTE9n.js";const N=C({__name:"MediaSourceSelect",props:{modelValue:{default:""},options:{},loading:{type:Boolean,default:!1},placeholder:{default:"请选择自媒体来源"},clearable:{type:Boolean,default:!0},filterable:{type:Boolean,default:!0},allowCreate:{type:Boolean,default:!1},disabled:{type:Boolean,default:!1},selectClass:{},selectStyle:{}},emits:["update:modelValue","change","visible-change"],setup(e,{emit:i}){const u=e,n=i,d=m(()=>(u.options||[]).map(l=>typeof l=="string"?{name:l}:{name:l.name,value:l.value})),f=m(()=>d.value.some(l=>l.name===u.modelValue)),b=l=>{n("visible-change",l)};return(l,t)=>{const r=g,y=V;return o(),s(y,{"model-value":e.modelValue,placeholder:e.placeholder,clearable:e.clearable,filterable:e.filterable,"allow-create":e.allowCreate,"default-first-option":e.allowCreate,disabled:e.disabled,loading:e.loading,class:S(e.selectClass),style:w(e.selectStyle),"onUpdate:modelValue":t[0]||(t[0]=a=>n("update:modelValue",a)),onChange:t[1]||(t[1]=a=>n("change",a)),onVisibleChange:b},{default:v(()=>[(o(!0),p(h,null,B(c(d),a=>(o(),s(r,{key:a.name,label:a.name,value:a.name},null,8,["label","value"]))),128)),e.modelValue&&!c(f)?(o(),s(r,{key:`__legacy_${e.modelValue}`,label:`${e.modelValue}(已停用)`,value:e.modelValue},null,8,["label","value"])):k("",!0)]),_:1},8,["model-value","placeholder","clearable","filterable","allow-create","default-first-option","disabled","loading","class","style"])}}});export{N as _};
|
||||
-1
@@ -1 +0,0 @@
|
||||
import{o as g,q as n,O as s,D as V,r as C,F as v,G as h,bn as B,u as c,P as p,t as w,ae as S,bm as k,p as i}from"./.pnpm-BSw4l71J.js";const z=g({__name:"MediaSourceSelect",props:{modelValue:{default:""},options:{},loading:{type:Boolean,default:!1},placeholder:{default:"请选择自媒体来源"},clearable:{type:Boolean,default:!0},filterable:{type:Boolean,default:!0},allowCreate:{type:Boolean,default:!1},disabled:{type:Boolean,default:!1},selectClass:{},selectStyle:{}},emits:["update:modelValue","change","visible-change"],setup(e,{emit:m}){const u=e,o=m,d=i(()=>(u.options||[]).map(l=>typeof l=="string"?{name:l}:{name:l.name,value:l.value})),f=i(()=>d.value.some(l=>l.name===u.modelValue)),b=l=>{o("visible-change",l)};return(l,t)=>{const r=B,y=k;return n(),s(y,{"model-value":e.modelValue,placeholder:e.placeholder,clearable:e.clearable,filterable:e.filterable,"allow-create":e.allowCreate,"default-first-option":e.allowCreate,disabled:e.disabled,loading:e.loading,class:S(e.selectClass),style:w(e.selectStyle),"onUpdate:modelValue":t[0]||(t[0]=a=>o("update:modelValue",a)),onChange:t[1]||(t[1]=a=>o("change",a)),onVisibleChange:b},{default:V(()=>[(n(!0),C(v,null,h(c(d),a=>(n(),s(r,{key:a.name,label:a.name,value:a.name},null,8,["label","value"]))),128)),e.modelValue&&!c(f)?(n(),s(r,{key:`__legacy_${e.modelValue}`,label:`${e.modelValue}(已停用)`,value:e.modelValue},null,8,["label","value"])):p("",!0)]),_:1},8,["model-value","placeholder","clearable","filterable","allow-create","default-first-option","disabled","loading","class","style"])}}});export{z as _};
|
||||
@@ -1 +0,0 @@
|
||||
.msg-bubble[data-v-b5ff1168]{display:flex;gap:10px;padding:10px 20px;align-items:flex-start}.msg-bubble.is-staff[data-v-b5ff1168]{flex-direction:row-reverse}.msg-bubble.is-staff .msg-body[data-v-b5ff1168]{align-items:flex-end}.msg-bubble.is-staff .msg-content.bubble-text[data-v-b5ff1168],.msg-bubble.is-staff .msg-content.bubble-file[data-v-b5ff1168]{background:#95ec69;color:#000}.msg-bubble .msg-avatar[data-v-b5ff1168]{flex-shrink:0;background:#d0d7de;color:#fff;font-size:12px}.msg-bubble .msg-body[data-v-b5ff1168]{display:flex;flex-direction:column;gap:4px;max-width:65%;min-width:0}.msg-bubble .msg-meta[data-v-b5ff1168]{display:flex;gap:8px;align-items:center;font-size:12px;color:#999}.msg-bubble .msg-sender[data-v-b5ff1168]{font-weight:500;color:#555}.msg-bubble .msg-content[data-v-b5ff1168]{padding:8px 12px;border-radius:6px;background:#fff;word-break:break-word;line-height:1.6;max-width:100%}.msg-bubble .msg-content.bubble-media[data-v-b5ff1168]{padding:0;background:transparent}.msg-bubble .content-text[data-v-b5ff1168]{white-space:pre-wrap}.msg-bubble .content-image[data-v-b5ff1168]{max-width:300px;max-height:300px;border-radius:6px;display:block}.msg-bubble .content-video[data-v-b5ff1168]{max-width:320px;max-height:320px;border-radius:6px;display:block}.msg-bubble .content-audio[data-v-b5ff1168]{max-width:260px}.msg-bubble .content-file[data-v-b5ff1168]{display:flex;gap:12px;align-items:center;min-width:220px;padding:4px 8px}.msg-bubble .content-file .file-meta[data-v-b5ff1168]{flex:1;min-width:0}.msg-bubble .content-file .file-name[data-v-b5ff1168]{font-size:14px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.msg-bubble .content-file .file-info[data-v-b5ff1168]{font-size:12px;color:#888}.msg-bubble .content-pending[data-v-b5ff1168]{display:flex;gap:6px;align-items:center;color:#999;font-size:13px}.msg-bubble .content-fallback .fallback-title[data-v-b5ff1168]{font-size:13px;color:#666;margin-bottom:4px}.msg-bubble .content-fallback .fallback-desc[data-v-b5ff1168]{font-size:14px;color:#333}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
.msg-bubble[data-v-80799394]{display:flex;gap:10px;padding:10px 20px;align-items:flex-start}.msg-bubble.is-staff[data-v-80799394]{flex-direction:row-reverse}.msg-bubble.is-staff .msg-body[data-v-80799394]{align-items:flex-end}.msg-bubble.is-staff .msg-content.bubble-text[data-v-80799394],.msg-bubble.is-staff .msg-content.bubble-file[data-v-80799394]{background:#95ec69;color:#000}.msg-bubble .msg-avatar[data-v-80799394]{flex-shrink:0;background:#d0d7de;color:#fff;font-size:12px}.msg-bubble .msg-body[data-v-80799394]{display:flex;flex-direction:column;gap:4px;max-width:65%;min-width:0}.msg-bubble .msg-meta[data-v-80799394]{display:flex;gap:8px;align-items:center;font-size:12px;color:#999}.msg-bubble .msg-sender[data-v-80799394]{font-weight:500;color:#555}.msg-bubble .msg-content[data-v-80799394]{padding:8px 12px;border-radius:6px;background:#fff;word-break:break-word;line-height:1.6;max-width:100%}.msg-bubble .msg-content.bubble-media[data-v-80799394]{padding:0;background:transparent}.msg-bubble .content-text[data-v-80799394]{white-space:pre-wrap}.msg-bubble .content-image[data-v-80799394]{max-width:300px;max-height:300px;border-radius:6px;display:block}.msg-bubble .content-video[data-v-80799394]{max-width:320px;max-height:320px;border-radius:6px;display:block}.msg-bubble .content-audio[data-v-80799394]{max-width:260px}.msg-bubble .content-file[data-v-80799394]{display:flex;gap:12px;align-items:center;min-width:220px;padding:4px 8px}.msg-bubble .content-file .file-meta[data-v-80799394]{flex:1;min-width:0}.msg-bubble .content-file .file-name[data-v-80799394]{font-size:14px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.msg-bubble .content-file .file-info[data-v-80799394]{font-size:12px;color:#888}.msg-bubble .content-pending[data-v-80799394]{display:flex;gap:6px;align-items:center;color:#999;font-size:13px}.msg-bubble .content-fallback .fallback-title[data-v-80799394]{font-size:13px;color:#666;margin-bottom:4px}.msg-bubble .content-fallback .fallback-desc[data-v-80799394]{font-size:14px;color:#333}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user