Merge origin/feat/730-xywang into feat/deploy-uat
# Conflicts:
# package-lock.json
Co-Authored-By: 王心悦 <wangxy84@xiaopeng.com>
| | |
| | | { |
| | | "name": "xp-robotaxi-management", |
| | | "version": "1.1.27", |
| | | "version": "1.2.5", |
| | | "lockfileVersion": 3, |
| | | "requires": true, |
| | | "packages": { |
| | | "": { |
| | | "name": "xp-robotaxi-management", |
| | | "version": "1.1.27", |
| | | "version": "1.2.5", |
| | | "license": "MIT", |
| | | "dependencies": { |
| | | "@vueuse/core": "^13.6.0", |
| | |
| | | <div |
| | | ref="editorRef" |
| | | class="ai-editor-host" |
| | | :style="{ minHeight: height }" |
| | | /> |
| | | <div |
| | | v-if="showCount" |
| | |
| | | .ai-editor-container { |
| | | position: relative; |
| | | width: 100%; |
| | | height: v-bind(height); |
| | | display: flex; |
| | | flex-direction: column; |
| | | min-height: 0; |
| | | border-radius: 4px; |
| | | background: #fff; |
| | | --aie-border-color: rgba(23, 31, 38, 0.15); |
| | |
| | | |
| | | .ai-editor-host { |
| | | width: 100%; |
| | | min-height: v-bind(height); |
| | | flex: 1; |
| | | min-height: 0; |
| | | } |
| | | |
| | | .ai-editor-count { |
| | |
| | | overflow: hidden; |
| | | border: 1px solid rgba(23, 31, 38, 0.15); |
| | | box-sizing: border-box; |
| | | min-height: 100%; |
| | | height: v-bind(height); |
| | | max-height: v-bind(height); |
| | | } |
| | | |
| | | :deep(.aie-container aie-header) { |
| | | background: #f4f6f8; |
| | | border-bottom: 1px solid rgba(23, 31, 38, 0.15); |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | :deep(.aie-container aie-header > div) { |
| | |
| | | } |
| | | |
| | | :deep(.aie-main) { |
| | | flex: 1; |
| | | min-height: v-bind(height); |
| | | flex: 1 1 auto; |
| | | min-height: 0; |
| | | overflow: auto; |
| | | } |
| | | |
| | | :deep(.aie-content) { |
| | | min-height: 100%; |
| | | padding-bottom: 28px; |
| | | padding-bottom: 40px; |
| | | box-sizing: border-box; |
| | | } |
| | | |
| | |
| | | version?: string |
| | | status?: number |
| | | sort?: number |
| | | agreementCategory?: number | null |
| | | }) { |
| | | return request.Post<Service.ResponseResult<any>>('/operation/api/v1/agreement/saveLoginAgreement', data) |
| | | } |
| | |
| | | updateBy: string |
| | | updateTime: string |
| | | version: string |
| | | latestAgreementCategory: number | null |
| | | }>>>(`/operation/api/v1/agreement/getLoginAgreementPageList/${type}`) |
| | | } |
| | | |
| | |
| | | updateBy: string |
| | | updateTime: string |
| | | version: string |
| | | agreementCategory: number | null |
| | | }> |
| | | size: number |
| | | total: number |
| | |
| | | updateBy: string |
| | | updateTime: string |
| | | version: string |
| | | agreementCategory: number | null |
| | | }>>(`/operation/api/v1/agreement/loginAgreement/detail/${id}`) |
| | | } |
| | | |
| | |
| | | import OperationLogModal from '@/components/custom/OperationLogModal.vue' |
| | | import AgreementVersionModal from './components/AgreementVersionModal.vue' |
| | | import type { AgreementVersionFormValue } from './components/AgreementVersionModal.vue' |
| | | import { getAgreementCategoryLabel } from './constants/agreementCategory' |
| | | import { |
| | | copyLoginAgreement, |
| | | disableLoginAgreement, |
| | |
| | | sort: number |
| | | isNew?: boolean |
| | | status?: 0 | 1 | 2 |
| | | agreementCategory?: number | null |
| | | } |
| | | |
| | | const route = useRoute() |
| | |
| | | id: row.id, |
| | | version: row.version, |
| | | content: row.content, |
| | | agreementCategory: row.agreementCategory ?? null, |
| | | } |
| | | } |
| | | const res = await getLoginAgreementDetail(row.id) |
| | |
| | | resizable: true, |
| | | }, |
| | | { |
| | | title: '协议类型', |
| | | key: 'agreementCategory', |
| | | width: 100, |
| | | resizable: true, |
| | | render: (row: AgreementVersionRow) => getAgreementCategoryLabel(row.agreementCategory), |
| | | }, |
| | | { |
| | | title: '状态', |
| | | key: 'status', |
| | | width: 140, |
| | |
| | | title: '操作', |
| | | key: 'actions', |
| | | width: 120, |
| | | fixed: 'right', |
| | | render: (row, index) => ( |
| | | <div class="flex items-center gap-12px"> |
| | | {isPendingStatus(row, index) |
| | |
| | | sort: Number(item.sort || index + 1), |
| | | isNew: false, |
| | | status: [0, 1, 2].includes(Number(item.status)) ? Number(item.status) as 0 | 1 | 2 : undefined, |
| | | agreementCategory: item.agreementCategory ?? null, |
| | | })) |
| | | } |
| | | |
| | |
| | | openVersionModal('新增版本', { |
| | | version, |
| | | content: '', |
| | | agreementCategory: null, |
| | | }) |
| | | } |
| | | |
| | |
| | | id: Number(detail.id), |
| | | version: detail.version || row.version, |
| | | content: detail.content || '', |
| | | agreementCategory: detail.agreementCategory ?? null, |
| | | }) |
| | | } |
| | | |
| | |
| | | id: Number(detail.id), |
| | | version: detail.version || row.version, |
| | | content: detail.content || '', |
| | | agreementCategory: detail.agreementCategory ?? null, |
| | | }, true) |
| | | } |
| | | |
| | |
| | | type: agreementType.value, |
| | | version: payload.version, |
| | | status: 0, |
| | | agreementCategory: payload.agreementCategory, |
| | | }) |
| | | if (res?.code !== 200) { |
| | | return |
| | |
| | | :bordered="false" |
| | | :row-key="(row: AgreementVersionRow) => row.id || row.version" |
| | | flex-height |
| | | :scroll-x="900" |
| | | :scroll-x="920" |
| | | > |
| | | <template #empty> |
| | | <div class="flex-center"> |
| | |
| | | .page-body { |
| | | height: calc(100vh - 88px - 28px); |
| | | overflow-y: auto; |
| | | overflow-x: hidden; |
| | | padding: 12px; |
| | | scrollbar-width: none; |
| | | } |
| | |
| | | getLoginAgreementPageList, |
| | | saveLoginAgreement, |
| | | } from "@/service/api/operations"; |
| | | import { getAgreementCategoryLabel } from "../constants/agreementCategory"; |
| | | import dayjs from "dayjs"; |
| | | |
| | | const props = defineProps<{ |
| | |
| | | updateTime: string; |
| | | sort: number; |
| | | version: string; |
| | | agreementCategory?: number | null; |
| | | } |
| | | |
| | | const router = useRouter(); |
| | |
| | | ]); |
| | | }, |
| | | }, |
| | | { title: titleLabel, resizable: true, key: "title" }, |
| | | { title: titleLabel, resizable: true, key: "title", width: 260, ellipsis: { tooltip: true } }, |
| | | { |
| | | title: "协议类型", |
| | | resizable: true, |
| | | key: "agreementCategory", |
| | | width: 140, |
| | | render: (row: AgreementItem) => getAgreementCategoryLabel(row.agreementCategory), |
| | | }, |
| | | { title: "启用中版本号", resizable: true, key: "version", width: 180 }, |
| | | { title: "创建人", resizable: true, key: "creator", width: 180 }, |
| | | { |
| | |
| | | title: "操作", |
| | | key: "actions", |
| | | width: 120, |
| | | fixed: "right", |
| | | render: (row: AgreementItem) => { |
| | | return h( |
| | | NSpace, |
| | |
| | | updateTime: item.updateTime || item.createTime || "", |
| | | sort: Number(item.sort || 0), |
| | | version: item.latestVersion || "-", |
| | | // 协议列表展示的协议类型取"生效中版本"的协议类型,与启用中版本号同源 |
| | | agreementCategory: item.latestAgreementCategory ?? null, |
| | | })); |
| | | } |
| | | |
| | |
| | | :bottom-bordered="true" |
| | | :row-key="(row) => row.id ?? row.nameId" |
| | | :row-props="getRowProps" |
| | | :scroll-x="1200" |
| | | class="custom-table" |
| | | > |
| | | <template #empty> |
| | |
| | | <script setup lang="ts"> |
| | | import { computed, reactive, ref, watch } from 'vue' |
| | | import type { FormInst, FormRules } from 'naive-ui' |
| | | import { NButton, NForm, NFormItem, NInput, NModal, NSpace } from 'naive-ui' |
| | | import { NButton, NForm, NFormItem, NInput, NModal, NSelect, NSpace } from 'naive-ui' |
| | | import AiRichTextEditor from '@/components/custom/Editor/AiRichTextEditor/index.vue' |
| | | import { AGREEMENT_CATEGORY_OPTIONS } from '../constants/agreementCategory' |
| | | |
| | | export interface AgreementVersionFormValue { |
| | | id?: number |
| | | version: string |
| | | content: string |
| | | agreementCategory?: number | null |
| | | } |
| | | |
| | | const props = withDefaults(defineProps<{ |
| | |
| | | const formModel = reactive<AgreementVersionFormValue>({ |
| | | version: '', |
| | | content: '', |
| | | agreementCategory: null, |
| | | }) |
| | | const contentErrorMessage = ref('') |
| | | |
| | | const plainTextLength = computed(() => { |
| | | return formModel.content |
| | |
| | | }) |
| | | |
| | | const rules: FormRules = { |
| | | content: [ |
| | | agreementCategory: [ |
| | | { |
| | | required: true, |
| | | validator: () => { |
| | | if (!plainTextLength.value) |
| | | return new Error('请输入协议内容') |
| | | // if (plainTextLength.value > 20000) |
| | | // return new Error('协议内容最多输入20000个字符') |
| | | return true |
| | | }, |
| | | trigger: ['blur', 'change', 'input'], |
| | | type: 'number', |
| | | message: '请选择协议类型', |
| | | trigger: ['blur', 'change'], |
| | | }, |
| | | ], |
| | | } |
| | |
| | | formModel.id = props.rowData?.id |
| | | formModel.version = props.rowData?.version || '' |
| | | formModel.content = props.rowData?.content || '' |
| | | formModel.agreementCategory = props.rowData?.agreementCategory ?? null |
| | | contentErrorMessage.value = '' |
| | | formRef.value?.restoreValidation() |
| | | } |
| | | |
| | |
| | | async function handleSave() { |
| | | if (props.readonly) |
| | | return |
| | | |
| | | |
| | | contentErrorMessage.value = plainTextLength.value ? '' : '请输入协议内容' |
| | | |
| | | try { |
| | | await formRef.value?.validate() |
| | | } catch (errors) { |
| | |
| | | return |
| | | } |
| | | |
| | | if (contentErrorMessage.value) |
| | | return |
| | | |
| | | emit('save', { |
| | | id: formModel.id, |
| | | version: formModel.version, |
| | | content: formModel.content, |
| | | agreementCategory: formModel.agreementCategory, |
| | | }) |
| | | } |
| | | |
| | | watch(() => formModel.content, () => { |
| | | if (contentErrorMessage.value && plainTextLength.value) |
| | | contentErrorMessage.value = '' |
| | | }) |
| | | |
| | | watch(() => props.visible, (visible) => { |
| | | if (visible) |
| | |
| | | preset="card" |
| | | :title="title" |
| | | class="agreement-version-modal" |
| | | :style="{ width: '1000px' }" |
| | | :style="{ width: '1000px', height: '90vh', maxHeight: '90vh', display: 'flex', flexDirection: 'column' }" |
| | | :header-style="{ flexShrink: '0' }" |
| | | :segmented="{ content: true, action: true }" |
| | | :content-style="{ padding: '24px 24px 0 24px' }" |
| | | :content-style="{ padding: '24px 24px 0 24px', display: 'flex', flexDirection: 'column', flex: '1', minHeight: '0', overflow: 'hidden' }" |
| | | @close="handleClose" |
| | | > |
| | | <NForm |
| | | ref="formRef" |
| | | :model="formModel" |
| | | :rules="rules" |
| | | label-placement="top" |
| | | require-mark-placement="left" |
| | | > |
| | | <NFormItem label="版本号" required> |
| | | <NInput v-model:value="formModel.version" disabled placeholder="系统自动生成" /> |
| | | </NFormItem> |
| | | <NFormItem label="协议内容" path="content" required> |
| | | <div class="agreement-version-body"> |
| | | <NForm |
| | | ref="formRef" |
| | | :model="formModel" |
| | | :rules="rules" |
| | | label-placement="top" |
| | | require-mark-placement="left" |
| | | class="agreement-fixed-form" |
| | | > |
| | | <div class="form-row"> |
| | | <NFormItem label="版本号" required class="form-row-item"> |
| | | <NInput v-model:value="formModel.version" disabled placeholder="系统自动生成" /> |
| | | </NFormItem> |
| | | <NFormItem label="协议类型" path="agreementCategory" required class="form-row-item"> |
| | | <NSelect |
| | | v-model:value="formModel.agreementCategory" |
| | | :options="AGREEMENT_CATEGORY_OPTIONS" |
| | | :disabled="readonly" |
| | | placeholder="请选择协议类型" |
| | | /> |
| | | </NFormItem> |
| | | </div> |
| | | </NForm> |
| | | |
| | | <div class="agreement-content-label"> |
| | | 协议内容 |
| | | <span v-if="!readonly" class="agreement-content-required">*</span> |
| | | </div> |
| | | <div class="agreement-content-area"> |
| | | <div class="rich-text-wrapper"> |
| | | <div class="editor-font-tip"> |
| | | 推荐字号:大标题(21)小标题(15)正文(14) |
| | | </div> |
| | | <AiRichTextEditor v-model="formModel.content" :read-only="readonly" height="360px" /> |
| | | <AiRichTextEditor v-model="formModel.content" :read-only="readonly" height="100%" /> |
| | | </div> |
| | | </NFormItem> |
| | | </NForm> |
| | | <div v-if="contentErrorMessage" class="agreement-content-error"> |
| | | {{ contentErrorMessage }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <template #action> |
| | | <NSpace justify="end" :size="8"> |
| | |
| | | </template> |
| | | |
| | | <style scoped lang="scss"> |
| | | .agreement-version-body { |
| | | display: flex; |
| | | flex-direction: column; |
| | | flex: 1; |
| | | min-height: 0; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .agreement-fixed-form { |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .form-row { |
| | | display: flex; |
| | | gap: 24px; |
| | | } |
| | | |
| | | .form-row-item { |
| | | flex: 1; |
| | | min-width: 0; |
| | | } |
| | | |
| | | .agreement-content-label { |
| | | flex-shrink: 0; |
| | | margin-bottom: 8px; |
| | | font-size: 14px; |
| | | color: #171f26; |
| | | } |
| | | |
| | | .agreement-content-required { |
| | | margin-left: 4px; |
| | | color: #d03050; |
| | | } |
| | | |
| | | .agreement-content-area { |
| | | flex: 1; |
| | | min-height: 0; |
| | | display: flex; |
| | | flex-direction: column; |
| | | padding-bottom: 24px; |
| | | } |
| | | |
| | | .agreement-content-error { |
| | | flex-shrink: 0; |
| | | padding-top: 6px; |
| | | font-size: 12px; |
| | | color: #d03050; |
| | | } |
| | | |
| | | .rich-text-wrapper { |
| | | position: relative; |
| | | width: 100%; |
| | | flex: 1; |
| | | min-height: 0; |
| | | display: flex; |
| | | flex-direction: column; |
| | | } |
| | | |
| | | .rich-text-wrapper :deep(.ai-editor-container) { |
| | | flex: 1; |
| | | min-height: 0; |
| | | } |
| | | |
| | | .editor-font-tip { |
| | |
| | | } |
| | | |
| | | .rich-text-wrapper :deep(.aie-container) { |
| | | min-height: 360px; |
| | | font-size: 14px; |
| | | } |
| | | </style> |
| | | |
| | | .rich-text-wrapper :deep(.aie-main) { |
| | | min-height: 360px; |
| | | } |
| | | |
| | | .content-count { |
| | | width: 100%; |
| | | margin-top: 8px; |
| | | text-align: right; |
| | | font-size: 12px; |
| | | color: rgba(23, 31, 38, 0.45); |
| | | } |
| | | |
| | | :deep(.agreement-version-modal .n-card-header) { |
| | | <style lang="scss"> |
| | | /* |
| | | * 以下规则不能加 scoped:preset="card" 的 NModal 实际渲染的 NCard/NCard 内部结构 |
| | | * 是由 naive-ui 在组件外部独立创建的 DOM 节点,不带当前组件的 data-v-xxx 属性, |
| | | * scoped 下的 :deep() 选择器无法命中,必须用全局样式规则。 |
| | | */ |
| | | .agreement-version-modal .n-card-header { |
| | | padding: 20px 24px; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | :deep(.agreement-version-modal .n-card-header__main) { |
| | | .agreement-version-modal .n-card-header__main { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: #171f26; |
| | | } |
| | | |
| | | :deep(.agreement-version-modal .n-card__action) { |
| | | .agreement-version-modal .n-card__action { |
| | | background-color: transparent !important; |
| | | padding: 16px 24px; |
| | | flex-shrink: 0; |
| | | } |
| | | </style> |
| New file |
| | |
| | | /** 登录协议-协议类型:主协议/子协议/共享协议 */ |
| | | export const AGREEMENT_CATEGORY_MAIN = 1 |
| | | export const AGREEMENT_CATEGORY_SUB = 2 |
| | | export const AGREEMENT_CATEGORY_SHARED = 3 |
| | | |
| | | export const AGREEMENT_CATEGORY_OPTIONS = [ |
| | | { label: '主协议', value: AGREEMENT_CATEGORY_MAIN }, |
| | | { label: '子协议', value: AGREEMENT_CATEGORY_SUB }, |
| | | { label: '共享协议', value: AGREEMENT_CATEGORY_SHARED }, |
| | | ] |
| | | |
| | | const AGREEMENT_CATEGORY_LABEL_MAP: Record<number, string> = { |
| | | [AGREEMENT_CATEGORY_MAIN]: '主协议', |
| | | [AGREEMENT_CATEGORY_SUB]: '子协议', |
| | | [AGREEMENT_CATEGORY_SHARED]: '共享协议', |
| | | } |
| | | |
| | | /** 存量协议未设置协议类型时返回空字符串,用于列表回显 */ |
| | | export function getAgreementCategoryLabel(value?: number | null): string { |
| | | if (value === null || value === undefined) |
| | | return '-' |
| | | return AGREEMENT_CATEGORY_LABEL_MAP[value] || '-' |
| | | } |
| | |
| | | .tab-scroll-area { |
| | | height: calc(100vh - 304px - 28px); |
| | | overflow-y: auto; |
| | | overflow-x: hidden; |
| | | padding-bottom: 40px; |
| | | scrollbar-width: thin; |
| | | scrollbar-color: rgba(23, 31, 38, 0.2) transparent; |
| | |
| | | .tab-scroll-area { |
| | | height: calc(100vh - 304px - 28px); |
| | | overflow-y: auto; |
| | | overflow-x: hidden; |
| | | padding-bottom: 40px; |
| | | scrollbar-width: thin; |
| | | scrollbar-color: rgba(23, 31, 38, 0.2) transparent; |