revert(iam): 统一登录 - 撤销 IAM Hub 接入

This commit is contained in:
2026-09-08 16:14:59 +08:00
parent 9206d5ea62
commit d0e2422155
860 changed files with 2371 additions and 15106 deletions
+1 -19
View File
@@ -28,25 +28,7 @@ export function getWorkWechatConfig() {
// 企业微信授权登录
export function workWechatLogin(params: { code: string }) {
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`
return request.post({ url: '/login/workWechatLogin', params: { ...params, terminal: config.terminal } })
}
// 绑定企业微信
+9 -16
View File
@@ -1,7 +1,7 @@
import { defineStore } from 'pinia'
import type { RouteRecordRaw } from 'vue-router'
import { getUserInfo, iamHubExchange, login, logout, workWechatLogin } from '@/api/user'
import { getUserInfo, login, logout, workWechatLogin } from '@/api/user'
import { TOKEN_KEY } from '@/enums/cacheEnums'
import { PageEnum } from '@/enums/pageEnum'
import router, { filterAsyncRoutes } from '@/router'
@@ -31,12 +31,6 @@ 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 = {}
@@ -51,7 +45,10 @@ const useUserStore = defineStore({
password: password
})
.then((data) => {
resolve(this.applyLoginResult(data))
this.token = data.token
this.isPaw = data.is_paw ?? 1
cache.set(TOKEN_KEY, data.token)
resolve(data)
})
.catch((error) => {
reject(error)
@@ -76,20 +73,16 @@ const useUserStore = defineStore({
return new Promise((resolve, reject) => {
workWechatLogin({ code })
.then((data) => {
resolve(this.applyLoginResult(data))
this.token = data.token
this.isPaw = data.is_paw ?? 1
cache.set(TOKEN_KEY, data.token)
resolve(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()
+10 -82
View File
@@ -11,13 +11,11 @@
<div class="text-center text-3xl font-medium mb-8">{{ config.web_name }}</div>
<!-- 企业微信自动授权中 -->
<div v-if="wxWorkAutoLogin || iamAutoLogin" class="text-center py-10">
<div v-if="wxWorkAutoLogin" 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">
{{ iamAutoLogin ? '公司统一身份登录中...' : '企业微信授权登录中...' }}
</div>
<div class="text-gray-500">企业微信授权登录中...</div>
</div>
<template v-else>
@@ -60,42 +58,21 @@
<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">
请使用企业微信扫描二维码登录
@@ -122,7 +99,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 { getIamHubConfig, getIamHubStartUrl, getWorkWechatConfig } from '@/api/user'
import { getWorkWechatConfig } from '@/api/user'
const passwordRef = shallowRef<InputInstance>()
const formRef = shallowRef<FormInstance>()
@@ -147,8 +124,6 @@ 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)
@@ -185,14 +160,8 @@ 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)
}
@@ -221,24 +190,6 @@ 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
}
// OIDC 跨站回跳期间路由初始化可能尚未结束;完整重载可让权限守卫
// 使用刚持久化的本地 token 重新生成菜单,避免停留在登录 loading。
window.location.replace(import.meta.env.BASE_URL)
} 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
@@ -271,7 +222,7 @@ const renderWxWorkQRCode = async () => {
lang: 'zh',
state: 'admin_login',
href: '',
self_redirect: false
self_redirect: false,
})
}
@@ -290,27 +241,6 @@ 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')
@@ -318,9 +248,7 @@ 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)
-11392
View File
File diff suppressed because one or more lines are too long
-7
View File
@@ -1,7 +0,0 @@
object=zyt admin IAM Hub adapter
changed_scope=staff/admin OIDC Authorization Code plus PKCE/state/nonce/JWT validation; IAM lazy provisioning and server binding; one-time local session exchange; centralized HMAC revocation; local password lockout; admin login UI and tracked production bundle
branch=master
original_commit=cf3fbdc5ef5799d8abc7e6c1a650612e40ab441b
modified_commit=bd0a3d3b961a8cd683828e908c9281099b527304
modified_content_sha256=0f0e168a972f2f7cea93ddb23b1da3f71812c00257216dee476b28d68c61ff5e
runtime_test=http://localhost:18081
-19
View File
@@ -1,19 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
TARGET=${1:-}
CONFIRM=${2:-}
ORIGINAL=cf3fbdc5ef5799d8abc7e6c1a650612e40ab441b
if [[ -z "$TARGET" || "$CONFIRM" != "--confirm-original" ]]; then
echo "usage: $0 TARGET --confirm-original" >&2
exit 2
fi
if [[ ! -d "$TARGET/.git" ]]; then
echo "target is not a git checkout" >&2
exit 3
fi
git -C "$TARGET" reset --hard "$ORIGINAL" >/dev/null
git -C "$TARGET" clean -fdx >/dev/null
echo ROLLBACK_ORIGINAL_COMMIT_RESTORED
echo ROLLBACK_HEAD=$(git -C "$TARGET" rev-parse HEAD)
test -z "$(git -C "$TARGET" status --porcelain)"
echo ROLLBACK_WORKTREE=CLEAN
-101
View File
@@ -1,101 +0,0 @@
ZYT IAM HUB ADMIN ADAPTER VERIFICATION
CHANGED_BRANCH=master
CHANGED_FIELD=zyt_admin.iam_subject / iam_managed / iam_revoked_at; zyt_iam_revocation_event; OIDC PKCE/state/nonce/JWT issuer-audience-signature-at_hash validation; IAM provisioning/binding; one-time local session exchange; admin company-login UI and tracked dist
ORIGINAL_COMMIT=cf3fbdc5ef5799d8abc7e6c1a650612e40ab441b
MODIFIED_COMMIT=bd0a3d3b961a8cd683828e908c9281099b527304
MODIFIED_CONTENT_SHA256=0f0e168a972f2f7cea93ddb23b1da3f71812c00257216dee476b28d68c61ff5e
ARTIFACTS
MODIFIED_FILE=/Users/long/Work/zyt/artifacts/MODIFIED_FILE
DIFF_FILE=/Users/long/Work/zyt/artifacts/DIFF_FILE
VERIFICATION=/Users/long/Work/zyt/artifacts/VERIFICATION.txt
ROLLBACK=/Users/long/Work/zyt/artifacts/ROLLBACK.sh
BASELINE
COMMAND=php server/tests/AdminMultiRoleRegressionTest.php && php server/tests/WecomPromotionPoolOperatorContractTest.php
INPUT=/Users/long/Work/zyt at cf3fbdc5ef5799d8abc7e6c1a650612e40ab441b before IAM adapter changes
LITERAL_OUTPUT=AdminMultiRoleRegressionTest passed
LITERAL_OUTPUT=WECOM_PROMOTION_POOL_OPERATOR_CONTRACT_OK
EXIT_STATUS=0
MODIFIED
COMMAND=./scripts/verify_iam_hub.sh
INPUT=/Users/long/Work/zyt at bd0a3d3b961a8cd683828e908c9281099b527304 with rebuilt server/public/admin
LITERAL_OUTPUT=ZYT_IAM_UNIT_CONTRACT=PASS
LITERAL_OUTPUT=ZYT_EXISTING_AUTH_REGRESSION=PASS
LITERAL_OUTPUT=ZYT_IAM_PHP_LINT=PASS
LITERAL_OUTPUT=ZYT_IAM_FRONTEND_DIST=PASS
LITERAL_OUTPUT=ZYT_IAM_MIGRATION_CONTRACT=PASS
LITERAL_OUTPUT=ZYT_IAM_ADAPTER_VERIFIED
EXIT_STATUS=0
FRONTEND_BUILD
COMMAND=PATH="$HOME/.nvm/versions/node/v22.22.3/bin:$PATH" npm run build
INPUT=/Users/long/Work/zyt/admin using package-lock.json dependencies
LITERAL_OUTPUT=✓ 3871 modules transformed.
LITERAL_OUTPUT=✓ built in 54.04s
LITERAL_OUTPUT=文件正在复制 ==> ../server/public/admin
LITERAL_OUTPUT=文件已复制 ==> ../server/public/admin
EXIT_STATUS=0
TYPECHECK_BOUNDARY
COMMAND=npm run type-check; search output for src/api/user.ts, src/stores/modules/user.ts and src/views/account/login.vue
INPUT=/Users/long/Work/zyt/admin
LITERAL_OUTPUT=TYPECHECK_EXIT_STATUS=2
LITERAL_OUTPUT=CHANGED_FILE_TYPE_ERRORS=NONE
LITERAL_OUTPUT=BASELINE_ERROR_COUNT=60
EXIT_STATUS=0
RESULT=the repository-wide Vue typecheck retains 60 existing errors outside the IAM files; all changed IAM frontend paths are absent from the error output and the production build succeeds.
ESLINT_BOUNDARY
COMMAND=ESLINT_USE_FLAT_CONFIG=false ./node_modules/.bin/eslint src/api/user.ts src/stores/modules/user.ts src/views/account/login.vue
INPUT=/Users/long/Work/zyt/admin with repository package-lock dependencies
LITERAL_OUTPUT=ESLint: 9.39.3
LITERAL_OUTPUT=Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './recommended' is not defined by "exports" in @vue/eslint-config-typescript/package.json
EXIT_STATUS=2
RESULT=the repository ESLint configuration fails before loading source files; changed files were formatted with the repository Prettier and validated by the successful production build.
MIGRATION_RUNTIME
COMMAND=apply server/sql/1.9.20260908/add_iam_hub_admin_identity.sql twice to an isolated MySQL 5.7 database and query information_schema
INPUT=ephemeral Docker MySQL 5.7 with a minimal zyt_admin table
LITERAL_OUTPUT=COLUMNS=iam_subject,iam_managed,iam_revoked_at
LITERAL_OUTPUT=UNIQUE_SUBJECT_INDEX=1
LITERAL_OUTPUT=REVOCATION_TABLE=1
LITERAL_OUTPUT=SECOND_RUN_EXIT_STATUS=0
EXIT_STATUS=0
RESULT=the migration is repeatable on MySQL 5.7 and creates the identity fields, unique subject index and revocation audit table.
BROWSER_OIDC_RUNTIME
COMMAND=CUA open http://localhost:18081/admin/login; click 公司员工统一登录; complete Keycloak authentication; read final URL and DOM
INPUT=isolated ZYT MySQL database, committed PHP/Vue build, IAM Hub zyt entitlement and Keycloak zyt client
LITERAL_OUTPUT=URL=http://localhost:18081/admin/workbench
LITERAL_OUTPUT=DOM menuitem=工作台
LITERAL_OUTPUT=DOM user=统一登录测试员工
LITERAL_OUTPUT=DOM heading=数据工作台
EXIT_STATUS=0
RESULT=the browser completed Authorization Code plus PKCE, lazy-provisioned/reused a passwordless IAM-managed ZYT administrator, consumed the fragment exchange code and entered the authorized workbench.
REVOCATION_RUNTIME
COMMAND=disable sso-e2e in IAM Hub; wait for zyt delivery; call ZYT userinfo with the previous token; query isolated ZYT database
INPUT=application=zyt webhook=http://host.docker.internal:18081/adminapi/iam/revocation reason=zyt integration e2e
LITERAL_OUTPUT=ZYT_REVOCATION disable_http=200 delivery=delivered access_before=1 access_after=-1 disable=1,revoked=1,session_expired=1,audits=1
EXIT_STATUS=0
RESULT=IAM Hub signed delivery reached the actual ZYT controller; the local administrator was disabled, every session expired and one audit event persisted.
REENABLE_RUNTIME
COMMAND=enable sso-e2e in IAM Hub; repeat browser OIDC login; query isolated ZYT database
INPUT=previously IAM-revoked ZYT administrator
LITERAL_OUTPUT=IAM_ZYT_EMPLOYEE_ENABLE=200
LITERAL_OUTPUT=URL=http://localhost:18081/admin/workbench
LITERAL_OUTPUT=ZYT_REENABLE_LOCAL account=iam_f58a2fdf8da48d159446,managed=1,disable=0,revoked_at=0,active_sessions=1,audit_events=1
EXIT_STATUS=0
RESULT=central re-enable plus the next successful SSO restored the IAM-revoked local administrator without bypassing local manual-disable semantics.
ROLLBACK
COMMAND=/Users/long/Work/zyt/artifacts/ROLLBACK.sh /tmp/zyt-iam-rollback-copy --confirm-original
INPUT=a separate clone containing modified commit bd0a3d3b961a8cd683828e908c9281099b527304
LITERAL_OUTPUT=ROLLBACK_ORIGINAL_COMMIT_RESTORED
LITERAL_OUTPUT=ROLLBACK_HEAD=cf3fbdc5ef5799d8abc7e6c1a650612e40ab441b
LITERAL_OUTPUT=ROLLBACK_WORKTREE=CLEAN
EXIT_STATUS=0
RESTORED_STATUS=separate copy restored to the pre-IAM commit and behavior; /Users/long/Work/zyt remains on the modified implementation.
-26
View File
@@ -1,26 +0,0 @@
# 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密钥只写部署环境,不提交仓库。
-28
View File
@@ -1,28 +0,0 @@
#!/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
-16
View File
@@ -1,16 +0,0 @@
[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
@@ -1,41 +0,0 @@
<?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,11 +16,6 @@ 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;
/**
* 管理员登录控制器
@@ -29,57 +24,7 @@ use think\facade\Log;
*/
class LoginController extends BaseAdminController
{
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);
}
public array $notNeedLogin = ['account', 'workWechatConfig', 'workWechatLogin', 'checkDbColumn', 'changeFirstPassword'];
/**
* @notes 账号登录
@@ -60,7 +60,7 @@ class LoginValidate extends BaseValidate
// 任一不满足都走原始密码校验。每次免密登录都会记一条 warning 日志,便于发现误开。
if (self::isLocalDebugLoginBypassEnabled()) {
$adminInfo = Admin::where('account', '=', $data['account'])
->field(['password,disable,iam_managed'])
->field(['password,disable'])
->findOrEmpty();
if ($adminInfo->isEmpty()) {
return '账号不存在';
@@ -68,9 +68,6 @@ 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'] ?? ''),
@@ -99,7 +96,7 @@ class LoginValidate extends BaseValidate
}
$adminInfo = Admin::where('account', '=', $data['account'])
->field(['password,disable,iam_managed'])
->field(['password,disable'])
->findOrEmpty();
if ($adminInfo->isEmpty()) {
@@ -110,10 +107,6 @@ class LoginValidate extends BaseValidate
return '账号已禁用';
}
if ((int) ($adminInfo['iam_managed'] ?? 0) === 1) {
return '请使用公司员工统一登录';
}
if (empty($adminInfo['password'])) {
$adminAccountSafeCache->record();
return '账号不存在';
@@ -1,13 +0,0 @@
<?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;
}
@@ -1,131 +0,0 @@
<?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;
}
}
@@ -1,38 +0,0 @@
<?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);
}
}
@@ -1,84 +0,0 @@
<?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 : [],
];
}
}
@@ -1,87 +0,0 @@
<?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];
}
}
@@ -1,23 +0,0 @@
<?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;
}
}
@@ -1,135 +0,0 @@
<?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;
}
}
@@ -1,113 +0,0 @@
<?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);
}
}
@@ -1,83 +0,0 @@
<?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;
}
}
}
@@ -1,33 +0,0 @@
<?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;
}
}
-19
View File
@@ -1,19 +0,0 @@
<?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
@@ -0,0 +1 @@
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};
@@ -1 +0,0 @@
import r from"./error-D1FuDlMf.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-J_eFER10.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};
@@ -1 +0,0 @@
import o from"./error-D1FuDlMf.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-J_eFER10.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};
@@ -0,0 +1 @@
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};
@@ -1 +0,0 @@
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
@@ -1 +0,0 @@
@@ -1 +0,0 @@
@@ -1 +0,0 @@
@@ -1 +0,0 @@
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,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#x27;");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
@@ -1 +0,0 @@
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};
@@ -1 +0,0 @@
.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
@@ -1 +0,0 @@
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
@@ -1 +0,0 @@
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
@@ -1,2 +0,0 @@
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
@@ -0,0 +1 @@
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
@@ -1 +0,0 @@
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-D-ZRccbe.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-J_eFER10.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 @@
.assign-log-col-hint[data-v-0a09e3d2]{margin-left:4px;vertical-align:middle;color:var(--el-text-color-secondary);cursor:help}
@@ -0,0 +1 @@
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};
@@ -0,0 +1 @@
.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{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};
@@ -0,0 +1 @@
.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)}
@@ -1 +0,0 @@
.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
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
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{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};
@@ -1 +0,0 @@
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-D-ZRccbe.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-J_eFER10.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};
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
.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
@@ -1 +0,0 @@
.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
@@ -0,0 +1 @@
.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)}
@@ -1 +0,0 @@
.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
@@ -0,0 +1 @@
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};
@@ -1 +0,0 @@
import{_ as o}from"./GancaoSubmissionReconcileButton.vue_vue_type_script_setup_true_lang-Cik25qR2.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-D-ZRccbe.js";import"./index-J_eFER10.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};
@@ -0,0 +1 @@
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 @@
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-D-ZRccbe.js";import{k as f}from"./index-J_eFER10.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
@@ -0,0 +1 @@
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};
@@ -0,0 +1 @@
import{_ as m}from"./MediaSourceSelect.vue_vue_type_script_setup_true_lang-CsbRGLkf.js";import"./.pnpm-BSw4l71J.js";export{m as default};
@@ -1 +0,0 @@
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 @@
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 _};

Some files were not shown because too many files have changed in this diff Show More