config.vue 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. <template>
  2. <div class="page-container">
  3. <el-card :header="t('Cloudflare Stream 配置')">
  4. <el-form :model="config" label-width="160px" style="max-width: 600px">
  5. <el-form-item label="Account ID" required>
  6. <el-input v-model="config.accountId" placeholder="Cloudflare Account ID" />
  7. <div class="form-tip">在 Cloudflare Dashboard 右侧可以找到</div>
  8. </el-form-item>
  9. <el-form-item label="Customer Subdomain" required>
  10. <el-input v-model="config.customerSubdomain" placeholder="xxx(不含 customer- 前缀)">
  11. <template #prepend>customer-</template>
  12. <template #append>.cloudflarestream.com</template>
  13. </el-input>
  14. <div class="form-tip">{{ t('播放域名的子域名部分') }}</div>
  15. </el-form-item>
  16. <el-form-item label="API Token">
  17. <el-input
  18. v-model="config.apiToken"
  19. type="password"
  20. placeholder="Cloudflare API Token(可选)"
  21. show-password
  22. />
  23. <div class="form-tip">
  24. {{ t('仅在前端直接调用 API 时需要(不推荐)') }}
  25. <br />
  26. {{ t('推荐通过后端代理调用,避免暴露 Token') }}
  27. </div>
  28. </el-form-item>
  29. <el-form-item>
  30. <el-button type="primary" @click="saveConfig">{{ t('保存配置') }}</el-button>
  31. <el-button @click="testConnection">{{ t('测试连接') }}</el-button>
  32. </el-form-item>
  33. </el-form>
  34. </el-card>
  35. <el-card :header="t('配置说明')" style="margin-top: 20px">
  36. <el-collapse>
  37. <el-collapse-item title="如何获取 Account ID" name="1">
  38. <ol>
  39. <li>
  40. 登录
  41. <a href="https://dash.cloudflare.com" target="_blank">Cloudflare Dashboard</a>
  42. </li>
  43. <li>选择 Stream 产品</li>
  44. <li>在页面右侧可以看到 Account ID</li>
  45. </ol>
  46. </el-collapse-item>
  47. <el-collapse-item :title="t('如何获取 Customer Subdomain')" name="2">
  48. <ol>
  49. <li>进入 Stream 产品页面</li>
  50. <li>上传或选择任意视频</li>
  51. <li>
  52. 查看播放地址,格式为
  53. <code>https://customer-xxx.cloudflarestream.com/{video_id}/...</code>
  54. </li>
  55. <li>
  56. <code>xxx</code>
  57. 部分就是 Customer Subdomain
  58. </li>
  59. </ol>
  60. </el-collapse-item>
  61. <el-collapse-item title="如何创建 API Token" name="3">
  62. <ol>
  63. <li>
  64. 访问
  65. <a href="https://dash.cloudflare.com/profile/api-tokens" target="_blank">API Tokens 页面</a>
  66. </li>
  67. <li>点击 "Create Token"</li>
  68. <li>选择 "Stream - Edit" 模板或自定义权限</li>
  69. <li>创建后复制 Token</li>
  70. </ol>
  71. <el-alert type="warning" :closable="false" style="margin-top: 10px">
  72. <strong>安全提示:</strong>
  73. 不要在前端代码中暴露 API Token。推荐在后端服务器中存储 Token,前端通过后端代理调用 API。
  74. </el-alert>
  75. </el-collapse-item>
  76. <el-collapse-item title="后端 API 代理示例" name="4"></el-collapse-item>
  77. </el-collapse>
  78. </el-card>
  79. <el-card :header="t('快速测试')" style="margin-top: 20px">
  80. <el-form label-width="100px" style="max-width: 800px">
  81. <el-form-item label="Video ID">
  82. <el-input v-model="testVideoId" placeholder="输入 Video ID 测试播放" style="width: 400px" />
  83. <el-button type="primary" style="margin-left: 10px" @click="testPlay" :disabled="!testVideoId">
  84. {{ t('测试播放') }}
  85. </el-button>
  86. </el-form-item>
  87. <el-form-item label="生成的地址" v-if="testVideoId && config.customerSubdomain">
  88. <div class="url-list">
  89. <div class="url-item">
  90. <span class="label">HLS:</span>
  91. <code>{{ testHlsUrl }}</code>
  92. <el-button link type="primary" @click="copyUrl(testHlsUrl)">{{ t('复制') }}</el-button>
  93. </div>
  94. <div class="url-item">
  95. <span class="label">DASH:</span>
  96. <code>{{ testDashUrl }}</code>
  97. <el-button link type="primary" @click="copyUrl(testDashUrl)">{{ t('复制') }}</el-button>
  98. </div>
  99. <div class="url-item">
  100. <span class="label">iframe:</span>
  101. <code>{{ testIframeUrl }}</code>
  102. <el-button link type="primary" @click="copyUrl(testIframeUrl)">{{ t('复制') }}</el-button>
  103. </div>
  104. </div>
  105. </el-form-item>
  106. </el-form>
  107. </el-card>
  108. <!-- 测试播放弹窗 -->
  109. <el-dialog v-model="playDialogVisible" :title="t('测试播放')" width="900px" destroy-on-close>
  110. <div class="player-container">
  111. <VideoPlayer
  112. v-if="playDialogVisible && testVideoId"
  113. :player-type="'hls'"
  114. :src="testHlsUrl"
  115. :autoplay="true"
  116. :controls="true"
  117. />
  118. </div>
  119. </el-dialog>
  120. </div>
  121. </template>
  122. <script setup lang="ts">
  123. import { ref, reactive, computed, onMounted } from 'vue'
  124. import { ElMessage } from 'element-plus'
  125. import VideoPlayer from '@/components/VideoPlayer.vue'
  126. import { useStreamStore } from '@/store/stream'
  127. import { useI18n } from 'vue-i18n'
  128. const { t } = useI18n()
  129. const streamStore = useStreamStore()
  130. const config = reactive({
  131. accountId: '',
  132. customerSubdomain: '',
  133. apiToken: ''
  134. })
  135. const testVideoId = ref('')
  136. const playDialogVisible = ref(false)
  137. const testHlsUrl = computed(() => {
  138. if (!config.customerSubdomain || !testVideoId.value) return ''
  139. return `https://customer-${config.customerSubdomain}.cloudflarestream.com/${testVideoId.value}/manifest/video.m3u8`
  140. })
  141. const testDashUrl = computed(() => {
  142. if (!config.customerSubdomain || !testVideoId.value) return ''
  143. return `https://customer-${config.customerSubdomain}.cloudflarestream.com/${testVideoId.value}/manifest/video.mpd`
  144. })
  145. const testIframeUrl = computed(() => {
  146. if (!config.customerSubdomain || !testVideoId.value) return ''
  147. return `https://customer-${config.customerSubdomain}.cloudflarestream.com/${testVideoId.value}/iframe`
  148. })
  149. function saveConfig() {
  150. streamStore.initConfig({
  151. accountId: config.accountId,
  152. customerSubdomain: config.customerSubdomain,
  153. apiToken: config.apiToken
  154. })
  155. ElMessage.success('配置已保存')
  156. }
  157. function testConnection() {
  158. if (!config.customerSubdomain) {
  159. ElMessage.warning('请先填写 Customer Subdomain')
  160. return
  161. }
  162. // 简单测试:尝试访问域名
  163. const testUrl = `https://customer-${config.customerSubdomain}.cloudflarestream.com`
  164. window.open(testUrl, '_blank')
  165. ElMessage.info('已打开测试页面,请在新窗口中确认域名是否正确')
  166. }
  167. function testPlay() {
  168. if (!testVideoId.value) {
  169. ElMessage.warning('请输入 Video ID')
  170. return
  171. }
  172. if (!config.customerSubdomain) {
  173. ElMessage.warning('请先配置 Customer Subdomain')
  174. return
  175. }
  176. playDialogVisible.value = true
  177. }
  178. async function copyUrl(url: string) {
  179. if (!url) return
  180. try {
  181. await navigator.clipboard.writeText(url)
  182. ElMessage.success('已复制')
  183. } catch {
  184. ElMessage.error('复制失败')
  185. }
  186. }
  187. onMounted(() => {
  188. streamStore.loadConfig()
  189. const savedConfig = streamStore.config
  190. config.accountId = savedConfig.accountId || ''
  191. config.customerSubdomain = savedConfig.customerSubdomain || ''
  192. config.apiToken = savedConfig.apiToken || ''
  193. })
  194. </script>
  195. <style lang="scss" scoped>
  196. .page-container {
  197. }
  198. .form-tip {
  199. margin-top: 5px;
  200. font-size: 12px;
  201. color: #909399;
  202. line-height: 1.6;
  203. }
  204. .code-block {
  205. background-color: #f5f7fa;
  206. padding: 15px;
  207. border-radius: var(--radius-base);
  208. font-size: 12px;
  209. line-height: 1.6;
  210. overflow-x: auto;
  211. white-space: pre-wrap;
  212. word-break: break-all;
  213. }
  214. .url-list {
  215. .url-item {
  216. display: flex;
  217. align-items: center;
  218. gap: 10px;
  219. margin-bottom: 8px;
  220. .label {
  221. width: 50px;
  222. font-weight: 500;
  223. }
  224. code {
  225. flex: 1;
  226. background-color: #f5f7fa;
  227. padding: 4px 8px;
  228. border-radius: var(--radius-base);
  229. font-size: 12px;
  230. word-break: break-all;
  231. }
  232. }
  233. }
  234. .player-container {
  235. width: 100%;
  236. height: 480px;
  237. background-color: #000;
  238. border-radius: var(--radius-base);
  239. overflow: hidden;
  240. }
  241. :deep(.el-collapse-item__header) {
  242. font-weight: 500;
  243. }
  244. :deep(.el-collapse-item__content) {
  245. padding-top: 10px;
  246. ol {
  247. padding-left: 20px;
  248. line-height: 2;
  249. }
  250. a {
  251. color: #409eff;
  252. }
  253. code {
  254. background-color: #f5f7fa;
  255. padding: 2px 6px;
  256. border-radius: var(--radius-base);
  257. font-size: 12px;
  258. }
  259. }
  260. </style>