u
This commit is contained in:
1 parent
32d423b759
commit
78eb811ff7
102 files changed
+12067
-209
No files matched your search
@@ -0,0 +1,80 @@
|
||||
<script setup lang="ts">
|
||||
// 最小用法:只喂 files + categories,上传 / 删除交给业务(组件不发请求)
|
||||
import { ref } from 'vue'
|
||||
import { G3Attachment } from '@g3soft/ui'
|
||||
import type { AttachmentCategory, AttachmentItem, AttachmentUploadContext } from '@g3soft/ui'
|
||||
|
||||
// 分类(可选):不给 categories 就只渲染「列表 + 预览」两栏
|
||||
const categories: AttachmentCategory[] = [
|
||||
{ id: 'contract', name: '合同文件' },
|
||||
{ id: 'invoice', name: '发票凭证' },
|
||||
{ id: 'photo', name: '现场照片' },
|
||||
]
|
||||
|
||||
// 内联 SVG,保证文档站离线也能看到图片预览
|
||||
const PHOTO = `data:image/svg+xml;utf8,${encodeURIComponent(
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="320" height="200"><rect width="320" height="200" fill="#e5e7eb"/><text x="160" y="108" font-size="18" text-anchor="middle" fill="#6b7280">现场照片</text></svg>',
|
||||
)}`
|
||||
|
||||
const files = ref<AttachmentItem[]>([
|
||||
{ id: 1, name: '2026年度框架合同.pdf', size: 512_000, categoryId: 'contract' },
|
||||
{ id: 2, name: '供应商报价明细.xlsx', size: 86_000, categoryId: 'invoice' },
|
||||
{ id: 3, name: '现场照片.jpg', size: 1_240_000, categoryId: 'photo', url: PHOTO },
|
||||
{ id: 4, name: '验收纪要.docx', size: 24_000, categoryId: 'contract' },
|
||||
])
|
||||
|
||||
const lastEvent = ref('等待操作…')
|
||||
|
||||
// 没传 upload-request 时,组件把待上传文件抛出来,落库由业务决定
|
||||
function onUpload(incoming: File[], context: AttachmentUploadContext) {
|
||||
files.value = [
|
||||
...files.value,
|
||||
...incoming.map((file, index) => ({
|
||||
id: `local-${Date.now()}-${index}`,
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
categoryId: context.categoryId,
|
||||
// 真实项目换成接口返回的地址;示例用 blob 地址即时预览
|
||||
url: URL.createObjectURL(file),
|
||||
})),
|
||||
]
|
||||
lastEvent.value = `upload:${incoming.map((file) => file.name).join('、')}`
|
||||
}
|
||||
|
||||
function onRemove(items: AttachmentItem[]) {
|
||||
const removed = new Set(items.map((item) => item.id))
|
||||
files.value = files.value.filter((file) => !removed.has(file.id))
|
||||
lastEvent.value = `remove:${items.map((item) => item.name).join('、')}`
|
||||
}
|
||||
|
||||
function onDownload(items: AttachmentItem[]) {
|
||||
lastEvent.value = `download:${items.map((item) => item.name).join('、')}`
|
||||
}
|
||||
|
||||
function onInvalid(info: { message: string }) {
|
||||
lastEvent.value = `invalid:${info.message}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<G3Attachment
|
||||
v-model:files="files"
|
||||
:categories="categories"
|
||||
:height="560"
|
||||
@upload="onUpload"
|
||||
@remove="onRemove"
|
||||
@download="onDownload"
|
||||
@invalid="onInvalid"
|
||||
/>
|
||||
<p class="hint">{{ lastEvent }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.hint {
|
||||
margin: 8px 0 0;
|
||||
color: #8c8c8c;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,58 @@
|
||||
<script setup lang="ts">
|
||||
// 弹窗用法:业务自己用 G3Modal 包一层,附件组件并不知道「弹窗」的存在
|
||||
import { ref } from 'vue'
|
||||
import { G3Attachment, G3Button, G3Modal } from '@g3soft/ui'
|
||||
import type { AttachmentItem, AttachmentUploadContext } from '@g3soft/ui'
|
||||
|
||||
const open = ref(false)
|
||||
|
||||
// 从空列表开始:正好看到空态投放区 → 入队(底栏变"脏")→ 上传的完整过程
|
||||
const files = ref<AttachmentItem[]>([])
|
||||
|
||||
const lastEvent = ref('')
|
||||
|
||||
function onUpload(incoming: File[], context: AttachmentUploadContext) {
|
||||
files.value = [
|
||||
...files.value,
|
||||
...incoming.map((file, index) => ({
|
||||
id: `local-${Date.now()}-${index}`,
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
categoryId: context.categoryId,
|
||||
url: URL.createObjectURL(file),
|
||||
})),
|
||||
]
|
||||
lastEvent.value = `已加入 ${incoming.length} 个文件`
|
||||
}
|
||||
|
||||
function onRemove(items: AttachmentItem[]) {
|
||||
const removed = new Set(items.map((item) => item.id))
|
||||
files.value = files.value.filter((file) => !removed.has(file.id))
|
||||
lastEvent.value = `已删除 ${items.length} 个文件`
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<G3Button @click="open = true">打开附件管理</G3Button>
|
||||
<p class="hint">{{ lastEvent }}</p>
|
||||
|
||||
<!-- 弹窗高度用 vh,附件组件再按 100% 撑满;不给 categories 就只有列表一栏 -->
|
||||
<G3Modal v-model:open="open" title="附件管理" width="80%" :fullscreen="false">
|
||||
<G3Attachment
|
||||
v-model:files="files"
|
||||
height="60vh"
|
||||
@upload="onUpload"
|
||||
@remove="onRemove"
|
||||
/>
|
||||
</G3Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.hint {
|
||||
margin: 8px 0 0;
|
||||
color: #8c8c8c;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
// 只读:隐藏上传 / 删除入口,只保留浏览与下载
|
||||
import { G3Attachment } from '@g3soft/ui'
|
||||
import type { AttachmentItem } from '@g3soft/ui'
|
||||
|
||||
const PHOTO = `data:image/svg+xml;utf8,${encodeURIComponent(
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="320" height="200"><rect width="320" height="200" fill="#dbeafe"/><text x="160" y="108" font-size="18" text-anchor="middle" fill="#1d4ed8">现场照片</text></svg>',
|
||||
)}`
|
||||
|
||||
const files: AttachmentItem[] = [
|
||||
{ id: 1, name: '立项批复.pdf', size: 320_000 },
|
||||
{ id: 2, name: '现场照片.jpg', size: 1_024_000, url: PHOTO },
|
||||
{ id: 3, name: '预算表.xlsx', size: 64_000 },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- readonly 同时关掉「全选 / 批量删除 / 底栏上传」,拖拽与粘贴也会失效 -->
|
||||
<G3Attachment :files="files" readonly :height="420" />
|
||||
</template>
|
||||
@@ -0,0 +1,61 @@
|
||||
<script setup lang="ts">
|
||||
// 多选与批量操作:勾选后工具栏浮出「下载 / 删除」,行为全部走事件
|
||||
import { ref } from 'vue'
|
||||
import { G3Attachment } from '@g3soft/ui'
|
||||
import type { AttachmentItem } from '@g3soft/ui'
|
||||
|
||||
const files = ref<AttachmentItem[]>([
|
||||
{ id: 'a1', name: '设备清单.xlsx', size: 128_000 },
|
||||
{ id: 'a2', name: '施工方案.pdf', size: 2_048_000 },
|
||||
{ id: 'a3', name: '进度汇报.pptx', size: 640_000 },
|
||||
{ id: 'a4', name: '材料验收单.docx', size: 32_000 },
|
||||
{ id: 'a5', name: '现场记录.zip', size: 8_192_000 },
|
||||
])
|
||||
|
||||
const selected = ref<AttachmentItem[]>([])
|
||||
const lastEvent = ref('')
|
||||
|
||||
function onSelect(items: AttachmentItem[]) {
|
||||
selected.value = items
|
||||
}
|
||||
|
||||
function onRemove(items: AttachmentItem[]) {
|
||||
const removed = new Set(items.map((item) => item.id))
|
||||
files.value = files.value.filter((file) => !removed.has(file.id))
|
||||
lastEvent.value = `remove:${items.map((item) => item.name).join('、')}`
|
||||
}
|
||||
|
||||
function onDownload(items: AttachmentItem[]) {
|
||||
// 真实项目里在这里调接口拿签名地址再下载
|
||||
lastEvent.value = `download:${items.map((item) => item.name).join('、')}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- 每页 3 条演示分页;builtin-download 关掉后「下载」只上报事件 -->
|
||||
<G3Attachment
|
||||
v-model:files="files"
|
||||
:height="420"
|
||||
:builtin-download="false"
|
||||
:show-size-changer="true"
|
||||
:page-size="3"
|
||||
:page-size-options="[3, 5]"
|
||||
@select="onSelect"
|
||||
@remove="onRemove"
|
||||
@download="onDownload"
|
||||
/>
|
||||
<p class="hint">
|
||||
已选 {{ selected.length }} 个<span v-if="selected.length">:{{ selected.map((item) => item.name).join('、') }}</span>
|
||||
<template v-if="lastEvent"> | {{ lastEvent }}</template>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.hint {
|
||||
margin: 8px 0 0;
|
||||
color: #8c8c8c;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
// 最基础用法:v-model 绑 boolean,true 表示已验证通过
|
||||
import { ref } from 'vue'
|
||||
import { G3SliderCaptcha } from '@g3soft/ui'
|
||||
|
||||
const passed = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="max-width: 360px">
|
||||
<G3SliderCaptcha v-model="passed" />
|
||||
<p style="margin: 8px 0 0; font-size: 13px">
|
||||
当前状态:{{ passed ? '已验证' : '未验证' }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
// 与表单配合:FormItem 负责 label 与校验;提交后复位滑块(真实场景是校验失败 / 接口报错时)
|
||||
import { reactive, ref } from 'vue'
|
||||
import { G3Button, G3Form, G3FormItem, G3Input, G3Message, G3SliderCaptcha } from '@g3soft/ui'
|
||||
|
||||
const model = reactive({ account: '', captcha: false })
|
||||
|
||||
const rules = {
|
||||
account: [{ required: true, message: '请输入账号' }],
|
||||
captcha: [{ validator: (value: unknown) => value === true, message: '请完成滑块验证' }],
|
||||
}
|
||||
|
||||
const captchaRef = ref<{ resume: () => void } | null>(null)
|
||||
|
||||
function onSubmit(values: Record<string, unknown>) {
|
||||
G3Message.success(`提交:${JSON.stringify(values)}`)
|
||||
// 每轮提交都要重新验证:清值 + 复位滑块
|
||||
model.captcha = false
|
||||
captchaRef.value?.resume()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<G3Form
|
||||
:model="model"
|
||||
:rules="rules"
|
||||
layout="vertical"
|
||||
label-align="left"
|
||||
:required-mark="false"
|
||||
style="max-width: 360px"
|
||||
@submit="onSubmit"
|
||||
>
|
||||
<G3FormItem name="account" label="账号">
|
||||
<G3Input v-model="model.account" placeholder="请输入账号" />
|
||||
</G3FormItem>
|
||||
<G3FormItem name="captcha">
|
||||
<G3SliderCaptcha ref="captchaRef" v-model="model.captcha" />
|
||||
</G3FormItem>
|
||||
<G3FormItem>
|
||||
<G3Button type="primary" block html-type="submit">提交</G3Button>
|
||||
</G3FormItem>
|
||||
</G3Form>
|
||||
</template>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
// 三种尺寸:高度分别取 --g3-control-height-sm / -height / -height-lg
|
||||
import { ref } from 'vue'
|
||||
import { G3SliderCaptcha } from '@g3soft/ui'
|
||||
|
||||
const small = ref(false)
|
||||
const medium = ref(false)
|
||||
const large = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="display: grid; gap: 12px; max-width: 360px">
|
||||
<G3SliderCaptcha v-model="small" size="small" />
|
||||
<G3SliderCaptcha v-model="medium" />
|
||||
<G3SliderCaptcha v-model="large" size="large" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
// 基础用法:v-model 输出 HTML,工具条内置字体/字号/上下标/表格/撤销重做
|
||||
import { ref } from 'vue'
|
||||
import { G3RichText } from '@g3soft/ui'
|
||||
|
||||
const html = ref('<p>你好,<strong>G3RichText</strong>!</p>')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<G3RichText v-model="html" placeholder="请输入正文…" />
|
||||
</template>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
// 通过 ref 调用 getText / getHTML / clear / focus
|
||||
import { ref } from 'vue'
|
||||
import { G3Button, G3RichText, G3Space } from '@g3soft/ui'
|
||||
import type { RichTextExpose } from '@g3soft/ui'
|
||||
|
||||
const html = ref('<p>富文本<b>正文</b>示例</p>')
|
||||
const editorRef = ref<RichTextExpose | null>(null)
|
||||
const plainText = ref('')
|
||||
|
||||
function showPlainText() {
|
||||
plainText.value = editorRef.value?.getText() ?? ''
|
||||
}
|
||||
|
||||
function clearContent() {
|
||||
editorRef.value?.clear()
|
||||
plainText.value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<G3Space vertical :size="12" style="display: flex">
|
||||
<G3Space :size="8">
|
||||
<G3Button size="small" type="outline" @click="showPlainText">取纯文本</G3Button>
|
||||
<G3Button size="small" type="outline" @click="clearContent">清空</G3Button>
|
||||
</G3Space>
|
||||
|
||||
<G3RichText ref="editorRef" v-model="html" />
|
||||
|
||||
<p v-if="plainText" style="font-size: 13px; color: #666">
|
||||
纯文本:{{ plainText }}
|
||||
</p>
|
||||
</G3Space>
|
||||
</template>
|
||||
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
// readonly 可聚焦可复制但不可编辑;disabled 整体灰化不可交互
|
||||
import { ref } from 'vue'
|
||||
import { G3RichText, G3Segmented, G3Space } from '@g3soft/ui'
|
||||
|
||||
const mode = ref<'normal' | 'readonly' | 'disabled'>('readonly')
|
||||
const html = ref('<p>这是一段<strong>只读</strong>的内容,可以选中复制,但不能编辑。</p>')
|
||||
|
||||
const options = [
|
||||
{ label: '只读', value: 'readonly' },
|
||||
{ label: '禁用', value: 'disabled' },
|
||||
{ label: '正常', value: 'normal' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<G3Space vertical :size="12" style="display: flex">
|
||||
<G3Segmented v-model="mode" :options="options" />
|
||||
|
||||
<G3RichText
|
||||
v-model="html"
|
||||
:readonly="mode === 'readonly'"
|
||||
:disabled="mode === 'disabled'"
|
||||
/>
|
||||
</G3Space>
|
||||
</template>
|
||||
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
// 图片/视频上传:注入 uploadImage / uploadVideo,返回 { fileUrl, ... } 元数据;
|
||||
// 上传完成后触发 upload-inline,把内联资源上报给外层(对齐邮件 inline 附件模型)
|
||||
import { ref } from 'vue'
|
||||
import { G3RichText } from '@g3soft/ui'
|
||||
import type { RichTextUploadResult } from '@g3soft/ui'
|
||||
|
||||
const html = ref('')
|
||||
|
||||
// 演示用假上传:用 FileReader 转 data URL,模拟「上传 → 返回 URL」的完整流程
|
||||
function fakeUpload(file: File): Promise<RichTextUploadResult> {
|
||||
return new Promise((resolve) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
resolve({
|
||||
fileUrl: reader.result as string,
|
||||
fileId: `demo-${Date.now()}`,
|
||||
fileName: file.name,
|
||||
fileSize: file.size,
|
||||
})
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
})
|
||||
}
|
||||
|
||||
const inlines = ref<RichTextUploadResult[]>([])
|
||||
|
||||
function onUploadInline(result: RichTextUploadResult, kind: 'image' | 'video') {
|
||||
inlines.value.push(result)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<G3RichText
|
||||
v-model="html"
|
||||
:upload-image="fakeUpload"
|
||||
:upload-video="fakeUpload"
|
||||
placeholder="点击工具条的图片/视频按钮上传…"
|
||||
@upload-inline="onUploadInline"
|
||||
/>
|
||||
|
||||
<p v-if="inlines.length" style="font-size: 13px; color: #666; margin-top: 12px">
|
||||
已上传 {{ inlines.length }} 个内联资源:{{ inlines.map((i) => i.fileName).join('、') }}
|
||||
</p>
|
||||
</template>
|
||||
@@ -1,36 +1,55 @@
|
||||
<script setup lang="ts">
|
||||
// delay:请求很快时根本不显示,避免「闪一下」;慢请求才出现
|
||||
import { ref } from 'vue'
|
||||
import { G3Button, G3Spin, G3Space } from '@g3soft/ui'
|
||||
import { G3Button, G3Segmented, G3Space, G3Spin } from '@g3soft/ui'
|
||||
|
||||
const fastLoading = ref(false)
|
||||
const slowLoading = ref(false)
|
||||
|
||||
async function run(duration: number, target: typeof fastLoading) {
|
||||
target.value = true
|
||||
await new Promise((resolve) => setTimeout(resolve, duration))
|
||||
target.value = false
|
||||
// 可调的 delay 时长,直观对比「延迟多久后才出现」
|
||||
const delay = ref(300)
|
||||
const delayOptions = [
|
||||
{ label: '0ms', value: 0 },
|
||||
{ label: '300ms', value: 300 },
|
||||
{ label: '800ms', value: 800 },
|
||||
]
|
||||
|
||||
// 注意:模板里 @click="run(200, fastLoading)" 会把 ref 自动解包成布尔值,
|
||||
// 因此这里不接收 ref 对象,而是接收「设置加载态」的回调,避免在布尔值上写 .value 报错。
|
||||
function run(duration: number, setLoading: (v: boolean) => void) {
|
||||
setLoading(true)
|
||||
setTimeout(() => setLoading(false), duration)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<G3Space vertical :size="16" style="display: flex">
|
||||
<G3Space :size="8">
|
||||
<G3Button size="small" type="outline" @click="run(200, fastLoading)">200ms 请求</G3Button>
|
||||
<G3Button size="small" type="outline" @click="run(1500, slowLoading)">1500ms 请求</G3Button>
|
||||
<G3Space :size="8" align="center">
|
||||
<span class="label">delay</span>
|
||||
<G3Segmented v-model="delay" :options="delayOptions" />
|
||||
</G3Space>
|
||||
|
||||
<G3Spin :spinning="fastLoading" :delay="400">
|
||||
<div class="box">内容区域</div>
|
||||
<G3Space :size="8">
|
||||
<G3Button size="small" type="outline" @click="run(200, (v) => (fastLoading = v))">200ms 请求</G3Button>
|
||||
<G3Button size="small" type="outline" @click="run(1500, (v) => (slowLoading = v))">1500ms 请求</G3Button>
|
||||
</G3Space>
|
||||
|
||||
<G3Spin :spinning="fastLoading" :delay="delay">
|
||||
<div class="box">200ms 请求(短于 delay 时不显示加载态)</div>
|
||||
</G3Spin>
|
||||
|
||||
<G3Spin :spinning="slowLoading" :delay="400">
|
||||
<div class="box">内容区域</div>
|
||||
<G3Spin :spinning="slowLoading" :delay="delay">
|
||||
<div class="box">1500ms 请求(超过 delay 后显示加载态)</div>
|
||||
</G3Spin>
|
||||
</G3Space>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.label {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.box {
|
||||
padding: 20px;
|
||||
border: 1px solid #d9d9d9;
|
||||
|
||||
Reference in new issue
Block a user